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:
@@ -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)
|
||||||
|
|||||||
@@ -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('请求超时,请稍后重试')
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export interface ApiResponse<T> {
|
|||||||
/** 响应消息 */
|
/** 响应消息 */
|
||||||
message: string
|
message: string
|
||||||
/** 响应数据 */
|
/** 响应数据 */
|
||||||
data: T
|
data: T | null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
7
frontend/admin/src/types/http.typecheck.ts
Normal file
7
frontend/admin/src/types/http.typecheck.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import type { ApiResponse } from './http'
|
||||||
|
|
||||||
|
export const nullableSuccessResponseContract: ApiResponse<{ ok: true }> = {
|
||||||
|
code: 0,
|
||||||
|
message: 'ok',
|
||||||
|
data: null,
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user