From f563d5cc99da3dee61d7cff4c2d7ea90ea458021 Mon Sep 17 00:00:00 2001 From: shangfangjian Date: Tue, 22 Sep 2026 02:24:50 +0800 Subject: [PATCH] =?UTF-8?q?fix(P0-00):=20HTTP=20client=20=E5=BF=85?= =?UTF-8?q?=E9=A1=BB=E4=BC=A0=E9=80=92=20method/body,=E4=BB=85=20body=20?= =?UTF-8?q?=E5=AD=98=E5=9C=A8=E6=97=B6=E8=AE=BE=20Content-Type?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- frontend/src/lib/http.test.ts | 65 +++++++++++++++++++++++++++++++++++ frontend/src/lib/http.ts | 9 ++--- 2 files changed, 70 insertions(+), 4 deletions(-) create mode 100644 frontend/src/lib/http.test.ts diff --git a/frontend/src/lib/http.test.ts b/frontend/src/lib/http.test.ts new file mode 100644 index 0000000..b9fb5e9 --- /dev/null +++ b/frontend/src/lib/http.test.ts @@ -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 = {} +// @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)?.['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)?.['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)?.['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)?.['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)?.['Content-Type'], undefined) +}) diff --git a/frontend/src/lib/http.ts b/frontend/src/lib/http.ts index ddfabb3..29217ff 100644 --- a/frontend/src/lib/http.ts +++ b/frontend/src/lib/http.ts @@ -49,10 +49,11 @@ async function request(path: string, options: RequestOptions = {}): Promise = body ? { 'Content-Type': 'application/json' } : {} + const res = await fetch(url, { signal: controller.signal, method, body, headers }) if (!res.ok) { const rawText = await res.text()