feat: root error boundary and network-error normalisation

A load() that rethrew (e.g. the profile overview) fell through to SvelteKit's
bare default error page, and a connection-refused fetch rejected as a raw
TypeError that blanked the view. Wrap fetch network failures in the client as
ApiError(status 0, network_error) so callers handle them uniformly, and add a
root +error.svelte with a friendly message plus Try again / Go home actions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-07-11 14:47:41 +02:00
parent d779fa2b97
commit 32d0a7e13b
5 changed files with 142 additions and 2 deletions

View File

@@ -80,6 +80,17 @@ describe('request error envelope parsing', () => {
expect(err.code).toBe('http_error');
});
it('wraps a network-level fetch rejection in ApiError (status 0)', async () => {
// Connection refused / offline / DNS: fetch rejects with a TypeError.
// It must surface as an ApiError, not a bare TypeError that crashes a
// load function into SvelteKit's default error page.
fetchSpy.mockRejectedValueOnce(new TypeError('Failed to fetch'));
const err = (await getManga('x').catch((e) => e)) as ApiError;
expect(err).toBeInstanceOf(ApiError);
expect(err.status).toBe(0);
expect(err.code).toBe('network_error');
});
it('treats empty 200/201 bodies as undefined (no JSON.parse crash)', async () => {
// Regression: addMangaToCollection is typed `void` and the
// backend returns 201 (created) / 200 (already there) with

View File

@@ -103,7 +103,21 @@ export async function request<T>(
// working. For same-origin requests this is a no-op compared to the
// default 'same-origin', so the same-origin happy path is
// unchanged.
const res = await fetch(`${BASE}${path}`, { credentials: 'include', ...init });
let res: Response;
try {
res = await fetch(`${BASE}${path}`, { credentials: 'include', ...init });
} catch {
// A network-level failure (connection refused, DNS, offline, blocked by
// CORS) rejects `fetch` with a bare TypeError instead of returning a
// response. Normalise it to an ApiError (status 0 = "no response reached
// us") so callers and SvelteKit load functions handle it uniformly
// instead of crashing on an unexpected TypeError.
throw new ApiError(
0,
'network_error',
'Could not reach the server. Check your connection and try again.'
);
}
if (!res.ok) {
let code = 'http_error';
let message = `${res.status} ${res.statusText}`;