diff --git a/package.json b/package.json index e360941..159bbd2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@uschuster/webapp-scaffold", - "version": "0.3.3", + "version": "0.3.4", "description": "Shared build scripts + Vite config factories for webapp-template-derived projects.", "type": "module", "bin": { diff --git a/src/core-fetch.ts b/src/core-fetch.ts index 3f6fd34..a165531 100644 --- a/src/core-fetch.ts +++ b/src/core-fetch.ts @@ -149,24 +149,25 @@ export function createCoreFetch(opts: CoreFetchOptions = {}): CoreFetch { } if (r.status === 401) opts.on401?.(req); - if (r.status === 409 && opts.on409) { - const body = await r.text(); - const resolved = opts.on409(req, body); - if (resolved !== undefined) { - return wrap(resolved, r.status, r.headers) as T; + if (r.status === 409) { + const [text, json] = await readBody(r); + if (opts.on409) { + const resolved = opts.on409(req, text); + if (resolved !== undefined) { + return wrap(resolved, r.status, r.headers) as T; + } } - // Fall through to error-formatting path below. - const bodyJson = safeJson(body); - const msg = opts.formatError?.(req, r, body, bodyJson) - ?? `${r.status} ${r.statusText}: ${body}`; - throw decorate409(new Error(msg), bodyJson); + const msg = opts.formatError?.(req, r, text, json) + ?? (json && typeof (json as Record).message === 'string' + ? String((json as Record).message) + : `${r.status} ${r.statusText}: ${text}`); + throw decorate409(new Error(msg), json); } if (!r.ok) { - const body = await r.text(); - const bodyJson = safeJson(body); - const msg = opts.formatError?.(req, r, body, bodyJson) - ?? `${r.status} ${r.statusText}: ${body}`; + const [text, json] = await readBody(r); + const msg = opts.formatError?.(req, r, text, json) + ?? `${r.status} ${r.statusText}: ${text}`; throw new Error(msg); } @@ -183,6 +184,22 @@ function safeJson(text: string): unknown | null { try { return JSON.parse(text); } catch { return null; } } +/** + * Read an error-response body. Prefer `.json()` (tests commonly mock only + * that); fall back to `.text()` if json() throws. Returns both the raw + * text (for `formatError` callers that want to log it) and the parsed + * JSON (may be null when the body isn't parseable). + */ +async function readBody(r: Response): Promise<[string, unknown | null]> { + try { + const j = await r.clone().json(); + return [JSON.stringify(j), j]; + } catch { + const text = await r.text().catch(() => ''); + return [text, safeJson(text)]; + } +} + /** Attach status/body to 409 errors so conflict-resolvers can reach them. */ function decorate409(err: Error, body: unknown): Error { (err as Error & { status?: number; body?: unknown }).status = 409;