diff --git a/lib/request.ts b/lib/request.ts index 576160c..085bf15 100644 --- a/lib/request.ts +++ b/lib/request.ts @@ -31,8 +31,15 @@ export const request = async (url: string, options?: RequestInit & { data?: a throw new UnauthorizedError("Unauthorized"); } if (!response.ok) { - const error = (await response.text()).slice(0, 10000); - console.info("HTTP request", { ...details, error }); + const rawError = (await response.text()).slice(0, 10000); + const isHtml = + rawError.trimStart().startsWith("]*>([^<]+)<\/title>/i)?.[1]?.trim() || "HTML error page" + : rawError.slice(0, 200); + console.info("HTTP request", { ...details, error: rawError.slice(0, 500) }); throw new Error(`Request failed: ${response.status} ${error}`); } console.info("HTTP request", details); diff --git a/tests/lib/request.test.ts b/tests/lib/request.test.ts new file mode 100644 index 0000000..9623539 --- /dev/null +++ b/tests/lib/request.test.ts @@ -0,0 +1,52 @@ +import { request, UnauthorizedError } from "@/lib/request"; + +beforeEach(() => { + jest.restoreAllMocks(); +}); + +const mockFetch = (status: number, body: string, headers: Record = {}) => { + jest.spyOn(globalThis, "fetch").mockResolvedValue({ + ok: status >= 200 && status < 300, + status, + headers: new Headers(headers), + json: () => Promise.resolve(JSON.parse(body)), + text: () => Promise.resolve(body), + } as Response); +}; + +describe("request", () => { + it("returns parsed JSON on success", async () => { + mockFetch(200, '{"id": 1}'); + const result = await request<{ id: number }>("https://api.example.com/test"); + expect(result).toEqual({ id: 1 }); + }); + + it("throws UnauthorizedError on 401", async () => { + mockFetch(401, "Unauthorized"); + await expect(request("https://api.example.com/test")).rejects.toThrow(UnauthorizedError); + }); + + it("includes plain-text error body in thrown error (truncated to 200 chars)", async () => { + const longError = "x".repeat(300); + mockFetch(500, longError); + await expect(request("https://api.example.com/test")).rejects.toThrow(`Request failed: 500 ${"x".repeat(200)}`); + }); + + it("extracts from HTML error responses instead of including raw HTML", async () => { + const html = "<!DOCTYPE html><html><head><title>Page not found

404

"; + mockFetch(404, html); + await expect(request("https://api.example.com/test")).rejects.toThrow("Request failed: 404 Page not found"); + }); + + it("falls back to 'HTML error page' when HTML has no ", async () => { + const html = "<!DOCTYPE html><html><body><h1>Error</h1></body></html>"; + mockFetch(404, html); + await expect(request("https://api.example.com/test")).rejects.toThrow("Request failed: 404 HTML error page"); + }); + + it("detects HTML via content-type header even without DOCTYPE", async () => { + const html = "<div>Some error</div>"; + mockFetch(500, html, { "content-type": "text/html; charset=utf-8" }); + await expect(request("https://api.example.com/test")).rejects.toThrow("Request failed: 500 HTML error page"); + }); +});