fix(P0-00): HTTP client 必须传递 method/body,仅 body 存在时设 Content-Type
request() 此前忽略 options.method(默认 GET)与 options.body,导致 POST/PUT/DELETE 全部以 GET 空 body 发出;且无条件设置 Content-Type 误污染无 body 请求。 修复: 显式传递 method(默认 GET) 与 body; 仅当 body 非空时加 Content-Type。 测试 frontend/src/lib/http.test.ts(5/5):GET/POST/PUT/DELETE 的 method/body/headers。 运行: node --experimental-transform-types frontend/src/lib/http.test.ts
This commit is contained in:
@@ -0,0 +1,65 @@
|
|||||||
|
/**
|
||||||
|
* P0-00: HTTP client method/body/headers 可信度测试。
|
||||||
|
* 运行: node --experimental-strip-types frontend/src/lib/http.test.ts
|
||||||
|
*
|
||||||
|
* 最小环境 polyfill:Node 22 自带 fetch/AbortController,本测试不触发 401 路径,
|
||||||
|
* 故 window.dispatchEvent 不会被调用,无需完整 DOM。
|
||||||
|
*/
|
||||||
|
import { test } from 'node:test'
|
||||||
|
import assert from 'node:assert/strict'
|
||||||
|
|
||||||
|
// 最小浏览器环境 polyfill(仅覆盖 http.ts 在 happy path 用到的全局)
|
||||||
|
const store: Record<string, string> = {}
|
||||||
|
// @ts-expect-error 测试用最小 window stub
|
||||||
|
globalThis.window = {
|
||||||
|
dispatchEvent: () => false,
|
||||||
|
localStorage: {
|
||||||
|
getItem: (k: string) => store[k] ?? null,
|
||||||
|
setItem: (k: string, v: string) => { store[k] = v },
|
||||||
|
removeItem: (k: string) => { delete store[k] },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// 捕获每次 fetch 的入参供断言
|
||||||
|
let lastInit: RequestInit | undefined
|
||||||
|
globalThis.fetch = async (_url: string, init?: RequestInit) => {
|
||||||
|
lastInit = init
|
||||||
|
return new Response(JSON.stringify({ ok: true }), { status: 200, headers: { 'content-type': 'application/json' } })
|
||||||
|
}
|
||||||
|
|
||||||
|
const { http } = await import('./http.ts')
|
||||||
|
|
||||||
|
test('GET: method=GET, 无 body, 无 Content-Type', async () => {
|
||||||
|
await http.get('/api/v1/matches')
|
||||||
|
assert.equal(lastInit?.method, 'GET')
|
||||||
|
assert.equal(lastInit?.body, undefined)
|
||||||
|
assert.equal((lastInit?.headers as Record<string, string>)?.['Content-Type'], undefined)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('POST: method=POST, 序列化 body, 有 Content-Type', async () => {
|
||||||
|
await http.post('/api/v1/matches', { a: 1 })
|
||||||
|
assert.equal(lastInit?.method, 'POST')
|
||||||
|
assert.equal(lastInit?.body, JSON.stringify({ a: 1 }))
|
||||||
|
assert.equal((lastInit?.headers as Record<string, string>)?.['Content-Type'], 'application/json')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('POST 空 body: 不设 Content-Type', async () => {
|
||||||
|
await http.post('/api/v1/matches', undefined)
|
||||||
|
assert.equal(lastInit?.method, 'POST')
|
||||||
|
assert.equal(lastInit?.body, undefined)
|
||||||
|
assert.equal((lastInit?.headers as Record<string, string>)?.['Content-Type'], undefined)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('PUT: method=PUT, 有 body 与 Content-Type', async () => {
|
||||||
|
await http.put('/api/v1/x', { b: 2 })
|
||||||
|
assert.equal(lastInit?.method, 'PUT')
|
||||||
|
assert.equal(lastInit?.body, JSON.stringify({ b: 2 }))
|
||||||
|
assert.equal((lastInit?.headers as Record<string, string>)?.['Content-Type'], 'application/json')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('DELETE: method=DELETE, 无 body, 无 Content-Type', async () => {
|
||||||
|
await http.delete('/api/v1/x/1')
|
||||||
|
assert.equal(lastInit?.method, 'DELETE')
|
||||||
|
assert.equal(lastInit?.body, undefined)
|
||||||
|
assert.equal((lastInit?.headers as Record<string, string>)?.['Content-Type'], undefined)
|
||||||
|
})
|
||||||
@@ -49,10 +49,11 @@ async function request<T>(path: string, options: RequestOptions = {}): Promise<T
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(url, {
|
const method = (options.method ?? 'GET').toUpperCase()
|
||||||
signal: controller.signal,
|
const body = options.body
|
||||||
headers: { 'Content-Type': 'application/json' },
|
// 仅当有 body 时设置 Content-Type,避免 GET/DELETE 等无 body 请求被误标
|
||||||
})
|
const headers: Record<string, string> = body ? { 'Content-Type': 'application/json' } : {}
|
||||||
|
const res = await fetch(url, { signal: controller.signal, method, body, headers })
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const rawText = await res.text()
|
const rawText = await res.text()
|
||||||
|
|||||||
Reference in New Issue
Block a user