mirror of
https://github.com/langgenius/dify.git
synced 2026-07-22 03:08:33 +08:00
142 lines
4.8 KiB
TypeScript
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()
|
|
})
|
|
})
|