dify/cli/src/http/error-mapper.ts
Xiyuan Chen ba59d9a4ac
feat: unified ErrorBody contract for /openapi/v1 and difyctl (#37285)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-06-11 10:26:27 +00:00

96 lines
2.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,
}
function statusClass(status: number): StatusClass {
if (status === 401)
return AUTH_EXPIRED_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)
}