From 320aa9476f934c9508d11f184362959c7a8adeef Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 29 May 2026 12:32:09 +0800 Subject: [PATCH] fix(frontend): ApiResponse data nullability contract - Change ApiResponse.data from T to T | null to match backend reality - Add compile-time type contract file (http.typecheck.ts) - Maintain backward compatibility with existing service calls - Add test for success response with null data Refs: review-fix-closure-2026-05-28 ApiResponse nullability --- frontend/admin/src/lib/http/client.test.ts | 16 ++++++++++++++++ frontend/admin/src/lib/http/client.ts | 4 ++-- frontend/admin/src/types/http.ts | 2 +- frontend/admin/src/types/http.typecheck.ts | 7 +++++++ 4 files changed, 26 insertions(+), 3 deletions(-) create mode 100644 frontend/admin/src/types/http.typecheck.ts diff --git a/frontend/admin/src/lib/http/client.test.ts b/frontend/admin/src/lib/http/client.test.ts index 2f91602..1563ca0 100644 --- a/frontend/admin/src/lib/http/client.test.ts +++ b/frontend/admin/src/lib/http/client.test.ts @@ -566,6 +566,22 @@ describe('http client', () => { }) }) + it('returns null when a successful response carries null data', async () => { + const fetchMock = vi.mocked(fetch) + fetchMock.mockResolvedValueOnce( + jsonResponse({ + code: 0, + message: 'ok', + data: null, + }), + ) + + const { get } = await loadModules() + const result = await get('/nullable-success', undefined, { auth: false }) + + expect(result).toBeNull() + }) + it('converts aborted requests into timeout AppErrors', async () => { vi.useFakeTimers() const fetchMock = vi.mocked(fetch) diff --git a/frontend/admin/src/lib/http/client.ts b/frontend/admin/src/lib/http/client.ts index 91bcb52..bbc1021 100644 --- a/frontend/admin/src/lib/http/client.ts +++ b/frontend/admin/src/lib/http/client.ts @@ -143,7 +143,7 @@ async function refreshAccessToken(): Promise { return cleanupSessionOnAuthFailure() } - return result.data + return result.data as TokenBundle } async function performTokenRefresh(): Promise { @@ -293,7 +293,7 @@ async function request(path: string, options: RequestOptions = {}): Promise { /** 响应消息 */ message: string /** 响应数据 */ - data: T + data: T | null } /** diff --git a/frontend/admin/src/types/http.typecheck.ts b/frontend/admin/src/types/http.typecheck.ts new file mode 100644 index 0000000..b7eac8b --- /dev/null +++ b/frontend/admin/src/types/http.typecheck.ts @@ -0,0 +1,7 @@ +import type { ApiResponse } from './http' + +export const nullableSuccessResponseContract: ApiResponse<{ ok: true }> = { + code: 0, + message: 'ok', + data: null, +}