Refuse a GET request with a body before fetching so it never marks the network offline
CI / ci (push) Successful in 1m34s
CI / ci (pull_request) Successful in 1m34s

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:
2026-10-05 07:56:33 -04:00
co-authored by Claude Opus 5.5
parent 0573b39f6d
commit ce2e94869a
2 changed files with 19 additions and 1 deletions
+6 -1
View File
@@ -37,7 +37,7 @@ export type ApiLocation = Pick<Location, 'origin' | 'pathname' | 'search' | 'ass
export type ApiRequest = {
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
// Sent as JSON.
// Sent as JSON. A GET must not have one.
body?: unknown
}
@@ -107,6 +107,11 @@ export function createApiClient(location: ApiLocation, network: NetworkStatus):
async function apiFetch<T>(path: string, request: ApiRequest = {}): Promise<T> {
const method = request.method ?? 'GET'
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)
if (response.status === 403) {
const error = await parseForbidden(method, path, response)
+13
View File
@@ -96,6 +96,19 @@ describe('OfflinePill', () => {
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 () => {
const api = renderPill()
vi.stubGlobal('fetch', vi.fn<typeof fetch>().mockRejectedValue(new DOMException('aborted', 'AbortError')))