Serialize the request body before fetching so it never marks the network offline
A body JSON.stringify cannot serialize, such as a cyclic one, threw inside the fetch try/catch and was recorded as a network failure, showing the offline pill. It now fails before the fetch with its own error. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -74,14 +74,17 @@ export function createApiClient(location: ApiLocation, network: NetworkStatus):
|
||||
}
|
||||
headers[CSRF_HEADER] = csrfToken
|
||||
}
|
||||
// Serialize before fetching so a body that cannot be sent is its own error
|
||||
// and never counts as a network failure.
|
||||
let json: string | undefined
|
||||
try {
|
||||
json = body === undefined ? undefined : JSON.stringify(body)
|
||||
} catch (error) {
|
||||
throw new Error(`${method} ${path}: request body is not serializable as JSON`, { cause: error })
|
||||
}
|
||||
let response: Response
|
||||
try {
|
||||
response = await fetch(path, {
|
||||
method,
|
||||
headers,
|
||||
credentials: 'same-origin',
|
||||
body: body === undefined ? undefined : JSON.stringify(body),
|
||||
})
|
||||
response = await fetch(path, { method, headers, credentials: 'same-origin', body: json })
|
||||
} catch (error) {
|
||||
// fetch rejects with a TypeError when the network fails; an abort is a
|
||||
// DOMException and says nothing about the network.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { act, render, screen } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ApiProvider, createApiClient, type ApiClient } from '../api/index.ts'
|
||||
import { fakeLocation, jsonResponse, stubFetch } from '../test/fetchStub.ts'
|
||||
import { fakeLocation, jsonResponse, ME, stubFetch } from '../test/fetchStub.ts'
|
||||
import { createNetworkStatus } from './networkStatus.ts'
|
||||
import { OfflinePill } from './OfflinePill.tsx'
|
||||
|
||||
@@ -80,6 +80,22 @@ describe('OfflinePill', () => {
|
||||
expect(screen.queryByRole('status')).toBeNull()
|
||||
})
|
||||
|
||||
it('ignores a request body that cannot be serialized', async () => {
|
||||
const api = renderPill()
|
||||
const fetchMock = stubFetch(jsonResponse(200, ME))
|
||||
await act(() => api.loadMe())
|
||||
const body: Record<string, unknown> = {}
|
||||
body.self = body
|
||||
|
||||
await act(() =>
|
||||
expect(api.apiFetch('/api/v1/plan', { method: 'PUT', body })).rejects.toThrow(
|
||||
'PUT /api/v1/plan: request body is not serializable as JSON',
|
||||
),
|
||||
)
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1)
|
||||
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')))
|
||||
|
||||
Reference in New Issue
Block a user