dify/cli/src/http/error-mapper.test.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

142 lines
4.8 KiB
TypeScript

import type { HttpClientError } from '@/errors/base'
import { describe, expect, it } from 'vitest'
import { ErrorCode } from '@/errors/codes'
import { classifyResponse } from './error-mapper'
function res(status: number, body: unknown): Response {
return new Response(typeof body === 'string' ? body : JSON.stringify(body), {
status,
headers: { 'content-type': 'application/json' },
})
}
const req = new Request('https://dify.test/openapi/v1/apps')
function classified(status: number, body: unknown): Promise<HttpClientError> {
return classifyResponse(req, res(status, body))
}
describe('classifyResponse — canonical ErrorBody', () => {
it('attaches the parsed body whole as serverError', async () => {
const body = {
code: 'invalid_param',
message: 'Request validation failed',
status: 422,
hint: 'check the page parameter',
details: [{ type: 'int_parsing', loc: ['page'], msg: 'must be >= 1' }],
}
const err = await classified(422, body)
expect(err.serverError).toEqual(body)
expect(err.message).toBe('Request validation failed')
expect(err.code).toBe(ErrorCode.Server4xxOther)
})
it('401 classifies by status as AuthExpired with CLI login hint', async () => {
const err = await classified(401, {
code: 'unauthorized',
message: 'session expired or revoked',
status: 401,
})
expect(err.code).toBe(ErrorCode.AuthExpired)
expect(err.hint).toBe("run 'difyctl auth login' to sign in again")
})
it('unknown future server code is data, not behavior — status bucket decides', async () => {
const err = await classified(409, {
code: 'some_future_code',
message: 'nope',
status: 409,
})
expect(err.code).toBe(ErrorCode.Server4xxOther)
expect(err.serverError?.code).toBe('some_future_code')
})
it('429 classifies as RateLimited (dedicated exit code) and keeps the server code', async () => {
const err = await classified(429, {
code: 'too_many_requests',
message: 'slow down',
status: 429,
})
expect(err.code).toBe(ErrorCode.RateLimited)
expect(err.exit()).toBe(7)
expect(err.serverError?.code).toBe('too_many_requests')
})
it('429 with no parseable ErrorBody falls back to a generic rate-limit message', async () => {
const err = await classified(429, 'not json')
expect(err.code).toBe(ErrorCode.RateLimited)
expect(err.serverError).toBeUndefined()
expect(err.message).toBe('too many requests')
})
})
describe('classifyResponse 403', () => {
it('maps 403 to AccessDenied (exit 4 bucket)', async () => {
const req403 = new Request('https://x/openapi/v1/apps/abc/dsl')
const res403 = new Response(
JSON.stringify({
code: 'unsupported_token_type',
message: 'unsupported_token_type',
status: 403,
}),
{ status: 403, headers: { 'content-type': 'application/json' } },
)
const err = await classifyResponse(req403, res403)
expect(err.code).toBe(ErrorCode.AccessDenied)
expect(err.message).toBe('unsupported_token_type')
})
it('403 with no parseable ErrorBody falls back to generic denied message', async () => {
const err = await classified(403, 'not json')
expect(err.code).toBe(ErrorCode.AccessDenied)
expect(err.message).toBe('not permitted')
})
})
describe('classifyResponse 426', () => {
it('maps 426 to VersionSkew (exit 6) and surfaces the server upgrade message', async () => {
const body = {
code: 'upgrade_required',
message: 'difyctl 0.1.0 is no longer supported; upgrade to >= 0.2.0.',
status: 426,
hint: 'Upgrade difyctl: https://docs.dify.ai/en/cli/install',
}
const err = await classified(426, body)
expect(err.code).toBe(ErrorCode.VersionSkew)
expect(err.exit()).toBe(6)
expect(err.message).toBe('difyctl 0.1.0 is no longer supported; upgrade to >= 0.2.0.')
expect(err.serverError?.code).toBe('upgrade_required')
})
it('426 with no parseable ErrorBody falls back to a version message', async () => {
const err = await classified(426, 'not json')
expect(err.code).toBe(ErrorCode.VersionSkew)
expect(err.message).toBe('client version no longer supported by the server')
})
})
describe('classifyResponse — non-conforming bodies (no fallback by design)', () => {
it('non-JSON body yields no serverError, classification by status', async () => {
const err = await classified(502, '<html>bad gateway</html>')
expect(err.code).toBe(ErrorCode.Server5xx)
expect(err.serverError).toBeUndefined()
})
it('RFC 8628 string error field yields no serverError and a generic message', async () => {
const err = await classified(400, { error: 'slow_down' })
expect(err.message).toBe('request failed (HTTP 400)')
expect(err.serverError).toBeUndefined()
})
})