diff --git a/__tests__/client.test.ts b/__tests__/client.test.ts index 40ff3c0..2faa874 100644 --- a/__tests__/client.test.ts +++ b/__tests__/client.test.ts @@ -112,3 +112,30 @@ describe("post", () => { await expect(post("/api/test", {})).rejects.toThrow("Bad request"); }); }); + +for (const [method, send] of [ + ["GET", () => get("/api/test")], + ["POST", () => post("/api/test", {})], +] as const) { + describe(`${method} response contract`, () => { + it.each([ + [false, { error: "Preferred", message: "Other" }, "Preferred"], + [false, { message: "Rejected" }, "Rejected"], + [false, {}, "Request failed: 503"], + [true, { status: "error", message: "Application rejected" }, "Application rejected"], + ])("retains error precedence for HTTP ok=%s and %j", async (ok, data, message) => { + mockFetch.mockResolvedValue({ ok, status: 503, json: async () => data }); + await expect(send()).rejects.toThrow(message); + }); + + it("propagates an invalid JSON response without retrying", async () => { + mockFetch.mockClear(); + mockFetch.mockResolvedValue({ + ok: false, + json: async () => { throw new SyntaxError("Invalid JSON"); }, + }); + await expect(send()).rejects.toThrow("Invalid JSON"); + expect(mockFetch).toHaveBeenCalledTimes(1); + }); + }); +} diff --git a/src/client.ts b/src/client.ts index d2d326a..d7239bc 100644 --- a/src/client.ts +++ b/src/client.ts @@ -7,22 +7,12 @@ export interface ApiResponse { [key: string]: unknown; } -export async function get( - path: string, - params?: Record, +async function request( + url: URL, + options: Pick, ): Promise { - const baseUrl = getBaseUrl(); - const url = new URL(path, baseUrl); - if (params) { - for (const [key, value] of Object.entries(params)) { - if (value !== undefined && value !== "") { - url.searchParams.set(key, value); - } - } - } - const response = await fetch(url.toString(), { - method: "GET", + ...options, headers: { "x-api-key": getApiKey(), "Content-Type": "application/json", @@ -38,27 +28,28 @@ export async function get( return data; } +export async function get( + path: string, + params?: Record, +): Promise { + const url = new URL(path, getBaseUrl()); + if (params) { + for (const [key, value] of Object.entries(params)) { + if (value !== undefined && value !== "") { + url.searchParams.set(key, value); + } + } + } + + return request(url, { method: "GET" }); +} + export async function post( path: string, body: Record, ): Promise { - const baseUrl = getBaseUrl(); - const url = new URL(path, baseUrl); - - const response = await fetch(url.toString(), { + return request(new URL(path, getBaseUrl()), { method: "POST", - headers: { - "x-api-key": getApiKey(), - "Content-Type": "application/json", - }, body: JSON.stringify(body), }); - - const data: ApiResponse = await response.json(); - - if (!response.ok || data.status === "error") { - throw new Error(data.error || data.message || `Request failed: ${response.status}`); - } - - return data; }