Refuse a GET request with a body before fetching so it never marks the network offline
fetch rejects a GET with a body with a TypeError before sending anything, which the client took for a network failure and showed the offline pill. apiFetch now refuses such a request with its own error before calling fetch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -37,7 +37,7 @@ export type ApiLocation = Pick<Location, 'origin' | 'pathname' | 'search' | 'ass
|
|||||||
|
|
||||||
export type ApiRequest = {
|
export type ApiRequest = {
|
||||||
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
|
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
|
||||||
// Sent as JSON.
|
// Sent as JSON. A GET must not have one.
|
||||||
body?: unknown
|
body?: unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,6 +107,11 @@ export function createApiClient(location: ApiLocation, network: NetworkStatus):
|
|||||||
async function apiFetch<T>(path: string, request: ApiRequest = {}): Promise<T> {
|
async function apiFetch<T>(path: string, request: ApiRequest = {}): Promise<T> {
|
||||||
const method = request.method ?? 'GET'
|
const method = request.method ?? 'GET'
|
||||||
const url = apiURL(location.origin, method, path)
|
const url = apiURL(location.origin, method, path)
|
||||||
|
// fetch refuses a GET with a body by rejecting with a TypeError, the same
|
||||||
|
// error as a network failure, so refuse it here before anything is sent.
|
||||||
|
if (method === 'GET' && request.body !== undefined) {
|
||||||
|
throw new Error(`GET ${path}: a GET request cannot carry a body`)
|
||||||
|
}
|
||||||
let response = await send(method, url, request.body)
|
let response = await send(method, url, request.body)
|
||||||
if (response.status === 403) {
|
if (response.status === 403) {
|
||||||
const error = await parseForbidden(method, path, response)
|
const error = await parseForbidden(method, path, response)
|
||||||
|
|||||||
@@ -96,6 +96,19 @@ describe('OfflinePill', () => {
|
|||||||
expect(screen.queryByRole('status')).toBeNull()
|
expect(screen.queryByRole('status')).toBeNull()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('ignores a GET request with a body, which fetch would refuse to send', async () => {
|
||||||
|
const api = renderPill()
|
||||||
|
const fetchMock = stubFetch()
|
||||||
|
|
||||||
|
await act(() =>
|
||||||
|
expect(api.apiFetch('/api/v1/plan', { body: { week: 1 } })).rejects.toThrow(
|
||||||
|
'GET /api/v1/plan: a GET request cannot carry a body',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
expect(fetchMock).not.toHaveBeenCalled()
|
||||||
|
expect(screen.queryByRole('status')).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
it('ignores an aborted request', async () => {
|
it('ignores an aborted request', async () => {
|
||||||
const api = renderPill()
|
const api = renderPill()
|
||||||
vi.stubGlobal('fetch', vi.fn<typeof fetch>().mockRejectedValue(new DOMException('aborted', 'AbortError')))
|
vi.stubGlobal('fetch', vi.fn<typeof fetch>().mockRejectedValue(new DOMException('aborted', 'AbortError')))
|
||||||
|
|||||||
Reference in New Issue
Block a user