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
This commit is contained in:
Your Name
2026-05-29 12:32:09 +08:00
parent f758297a6e
commit 320aa9476f
4 changed files with 26 additions and 3 deletions

View File

@@ -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<null>('/nullable-success', undefined, { auth: false })
expect(result).toBeNull()
})
it('converts aborted requests into timeout AppErrors', async () => { it('converts aborted requests into timeout AppErrors', async () => {
vi.useFakeTimers() vi.useFakeTimers()
const fetchMock = vi.mocked(fetch) const fetchMock = vi.mocked(fetch)

View File

@@ -143,7 +143,7 @@ async function refreshAccessToken(): Promise<TokenBundle> {
return cleanupSessionOnAuthFailure() return cleanupSessionOnAuthFailure()
} }
return result.data return result.data as TokenBundle
} }
async function performTokenRefresh(): Promise<TokenBundle> { async function performTokenRefresh(): Promise<TokenBundle> {
@@ -293,7 +293,7 @@ async function request<T>(path: string, options: RequestOptions = {}): Promise<T
throw AppError.fromResponse(result, response.status) throw AppError.fromResponse(result, response.status)
} }
return result.data return result.data!
} catch (error) { } catch (error) {
if (error instanceof DOMException && error.name === 'AbortError') { if (error instanceof DOMException && error.name === 'AbortError') {
throw AppError.network('请求超时,请稍后重试') throw AppError.network('请求超时,请稍后重试')

View File

@@ -11,7 +11,7 @@ export interface ApiResponse<T> {
/** 响应消息 */ /** 响应消息 */
message: string message: string
/** 响应数据 */ /** 响应数据 */
data: T data: T | null
} }
/** /**

View File

@@ -0,0 +1,7 @@
import type { ApiResponse } from './http'
export const nullableSuccessResponseContract: ApiResponse<{ ok: true }> = {
code: 0,
message: 'ok',
data: null,
}