server-kit.test.js
4.87 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
import { describe, it, expect, vi } from 'vitest'
import Fastify from 'fastify'
import {
registerRawBodyParser,
registerTrace,
registerErrorHandler,
createAiLimit,
CODE_STATUS,
} from '../src/index.js'
function silentLogger() {
const child = () => ({ error: vi.fn(), info: vi.fn() })
return { child, info: vi.fn() }
}
function build({ limiter } = {}) {
const app = Fastify({ logger: false })
registerRawBodyParser(app)
registerTrace(app, { logger: silentLogger() })
registerErrorHandler(app)
if (limiter) {
const aiLimit = createAiLimit({ limiter })
app.post('/llm', { preHandler: aiLimit('llm') }, async () => ({ ok: true }))
}
return app
}
describe('server-kit 装配骨架', () => {
it('trace hook 回带 x-trace-id,rawBody 标记路由拿到原始字节', async () => {
const app = build()
let seenRaw = null
app.post('/hook', { config: { rawBody: true } }, async (request) => {
seenRaw = request.rawBody
return { ok: true }
})
const res = await app.inject({ method: 'POST', url: '/hook', payload: { a: 1 } })
expect(res.headers['x-trace-id']).toBeTruthy()
expect(Buffer.isBuffer(seenRaw)).toBe(true)
expect(JSON.parse(seenRaw.toString('utf8'))).toEqual({ a: 1 })
})
it('错误码 → HTTP 状态映射,且 status<500 透传 details', async () => {
const app = build()
app.get('/boom', async () => {
const err = new Error('nope')
err.code = 'INSUFFICIENT_BALANCE'
err.details = { needed: 5 }
throw err
})
const res = await app.inject({ method: 'GET', url: '/boom' })
expect(res.statusCode).toBe(402)
const body = res.json()
expect(body.error.code).toBe('INSUFFICIENT_BALANCE')
expect(body.error.details.needed).toBe(5)
expect(body.error.details.requestId).toBeTruthy()
expect(body.error.details.traceId).toBe(res.headers['x-trace-id'])
expect(CODE_STATUS.INSUFFICIENT_BALANCE).toBe(402)
})
it('waiting list 预期业务错误不退化成 500', async () => {
const app = build()
app.get('/waitlist-expired', async () => {
const err = new Error('Invite token expired')
err.code = 'waitlist.invite_expired'
throw err
})
const res = await app.inject({ method: 'GET', url: '/waitlist-expired' })
expect(res.statusCode).toBe(400)
expect(res.json().error).toMatchObject({
code: 'waitlist.invite_expired',
message: 'Invite token expired',
retryable: false,
})
expect(CODE_STATUS['waitlist.invite_not_found']).toBe(404)
})
it('waiting list 邀请投递失败映射为可识别的上游错误', async () => {
const app = build()
app.post('/waitlist-delivery', async () => {
const err = new Error('Waitlist invite delivery failed')
err.code = 'waitlist.invite_delivery_failed'
throw err
})
const res = await app.inject({ method: 'POST', url: '/waitlist-delivery' })
expect(res.statusCode).toBe(502)
expect(res.json().error).toMatchObject({
code: 'waitlist.invite_delivery_failed',
message: 'Internal server error',
retryable: true,
})
expect(CODE_STATUS['waitlist.invite_delivery_failed']).toBe(502)
})
it('服务器错误按 trace 记录结构化日志', async () => {
const error = vi.fn()
const logger = { child: () => ({ error }), info: vi.fn() }
const app = Fastify({ logger: false })
registerTrace(app, { logger })
registerErrorHandler(app)
app.get('/logged-error', async () => {
throw new Error('logged failure')
})
const res = await app.inject({ method: 'GET', url: '/logged-error' })
const body = res.json()
expect(error).toHaveBeenCalledWith('request failed', expect.objectContaining({
err: expect.any(Error),
status: 500,
traceId: res.headers['x-trace-id'],
requestId: body.error.details.requestId,
}))
})
it('500 类错误不泄漏内部信息、不透传 details', async () => {
const app = build()
app.get('/oops', async () => {
const err = new Error('secret stack')
err.details = { internal: true }
throw err
})
const res = await app.inject({ method: 'GET', url: '/oops' })
expect(res.statusCode).toBe(500)
const body = res.json()
expect(body.error.message).toBe('Internal server error')
expect(body.error.details.internal).toBeUndefined()
expect(body.error.details.requestId).toBeTruthy()
expect(body.error.details.traceId).toBe(res.headers['x-trace-id'])
})
it('aiLimit 超限返 429 + retry-after', async () => {
const limiter = { consume: vi.fn(async () => ({ allowed: false, remaining: 0, retryAfterMs: 5000, ruleId: 'llm:ip' })) }
const app = build({ limiter })
const res = await app.inject({ method: 'POST', url: '/llm', payload: {} })
expect(res.statusCode).toBe(429)
expect(res.headers['retry-after']).toBe('5')
expect(res.json().error.code).toBe('rate_limited')
})
})