dify/cli/src/http/error-mapper.ts
Stephen Zhou a84c2d36a3
style: format with vp fmt (#38803)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-07-12 15:57:46 +00:00

120 lines
3.8 KiB
TypeScript

import type { ErrorBody } from '@dify/contracts/api/openapi/types.gen'
import type { ErrorCodeValue } from '@/errors/codes'
import { zErrorBody } from '@dify/contracts/api/openapi/zod.gen'
import { BaseError, HttpClientError, newError } from '@/errors/base'
import { ErrorCode } from '@/errors/codes'
import { redactBearer } from './sanitize'
const AUTH_EXPIRED_MESSAGE = 'session expired or revoked'
const AUTH_LOGIN_HINT = "run 'difyctl auth login' to sign in again"
// How one HTTP status bucket classifies: CLI code, message fallback when the
// body is not a canonical ErrorBody, optional CLI hint, raw-body retention.
type StatusClass = {
readonly code: ErrorCodeValue
readonly fallbackMessage: (status: number) => string
readonly hint?: string
readonly includeRaw: boolean
}
const AUTH_EXPIRED_CLASS: StatusClass = {
code: ErrorCode.AuthExpired,
fallbackMessage: () => AUTH_EXPIRED_MESSAGE,
hint: AUTH_LOGIN_HINT,
includeRaw: false,
}
const SERVER_5XX_CLASS: StatusClass = {
code: ErrorCode.Server5xx,
fallbackMessage: (status) => `server error (HTTP ${status})`,
includeRaw: true,
}
const SERVER_4XX_CLASS: StatusClass = {
code: ErrorCode.Server4xxOther,
fallbackMessage: (status) => `request failed (HTTP ${status})`,
includeRaw: true,
}
// 429 gets a dedicated CLI code (its own exit code) so wrappers can tell a rate limit from a hard
// failure. The serverError.code ("too_many_requests" / "rate_limit_error") still rides along.
const RATE_LIMITED_CLASS: StatusClass = {
code: ErrorCode.RateLimited,
fallbackMessage: () => 'too many requests',
includeRaw: false,
}
const ACCESS_DENIED_CLASS: StatusClass = {
code: ErrorCode.AccessDenied,
fallbackMessage: () => 'not permitted',
includeRaw: false,
}
// 426 Upgrade Required: the server rejected this difyctl as too old. Give it the
// version-compat exit code so scripts can tell it apart from a generic failure.
// The server's ErrorBody.code ("upgrade_required") + message still ride along.
const VERSION_COMPAT_CLASS: StatusClass = {
code: ErrorCode.VersionSkew,
fallbackMessage: () => 'client version no longer supported by the server',
includeRaw: false,
}
function statusClass(status: number): StatusClass {
if (status === 401) return AUTH_EXPIRED_CLASS
if (status === 403) return ACCESS_DENIED_CLASS
if (status === 426) return VERSION_COMPAT_CLASS
if (status === 429) return RATE_LIMITED_CLASS
if (status >= 500) return SERVER_5XX_CLASS
return SERVER_4XX_CLASS
}
function parseServerError(raw: string): ErrorBody | undefined {
if (raw === '') return undefined
let parsed: unknown
try {
parsed = JSON.parse(raw)
} catch {
return undefined
}
const result = zErrorBody.safeParse(parsed)
return result.success ? result.data : undefined
}
export async function classifyResponse(
request: Request,
response: Response,
): Promise<HttpClientError> {
let raw = ''
try {
raw = await response.clone().text()
} catch {
// ignore read errors; raw stays ''
}
const serverError = parseServerError(raw)
const status = response.status
const c = statusClass(status)
return new HttpClientError({
code: c.code,
message: serverError?.message ?? c.fallbackMessage(status),
hint: c.hint,
httpStatus: status,
method: request.method,
url: redactBearer(response.url || request.url),
rawResponse: c.includeRaw && raw !== '' ? raw : undefined,
serverError,
})
}
export function classifyTransportError(err: unknown): BaseError {
if (err instanceof BaseError) {
return err
}
if (!(err instanceof Error)) {
return newError(ErrorCode.Unknown, String(err)).wrap(err)
}
const sanitized = redactBearer(err.message)
// there isn't a practical way to classify network errors reliably
return newError(ErrorCode.NetworkConnection, sanitized).wrap(err)
}