From 68d3a842e6d6801eee715087f2f992b0302cba0c Mon Sep 17 00:00:00 2001 From: burak33bb Date: Sat, 15 Aug 2026 05:18:25 +0300 Subject: [PATCH 1/2] fix(core): preserve HTTP error details --- packages/core/src/client/HttpClient.ts | 43 ++++++++++++++++++- packages/core/src/client/SapiomClient.test.ts | 34 +++++++++++++++ 2 files changed, 75 insertions(+), 2 deletions(-) diff --git a/packages/core/src/client/HttpClient.ts b/packages/core/src/client/HttpClient.ts index c1176a621..c5a885788 100644 --- a/packages/core/src/client/HttpClient.ts +++ b/packages/core/src/client/HttpClient.ts @@ -1,5 +1,7 @@ import { randomUUID } from "node:crypto"; +import type { HttpError, HttpResponse } from "../types/http.js"; + /** * Request configuration for the HTTP client */ @@ -35,6 +37,31 @@ export interface HttpClientConfig { versionPrefix?: string | null; } +class HttpRequestError extends Error implements HttpError { + public readonly status: number; + public readonly statusText: string; + public readonly headers: Record; + public readonly data: any; + public readonly response: HttpResponse; + + constructor(response: Response, data: any, headers: Record) { + super( + `Request failed with status ${response.status}: ${JSON.stringify(data)}`, + ); + this.name = "HttpRequestError"; + this.status = response.status; + this.statusText = response.statusText || ""; + this.headers = headers; + this.data = data; + this.response = { + status: response.status, + statusText: this.statusText, + headers, + data, + }; + } +} + /** * Core HTTP client for making API requests. * Uses native fetch for zero external dependencies. @@ -172,8 +199,10 @@ export class HttpClient { if (!response.ok) { const errorData = await this.parseResponse(response); - throw new Error( - `Request failed with status ${response.status}: ${JSON.stringify(errorData)}`, + throw new HttpRequestError( + response, + errorData, + this.responseHeaders(response), ); } @@ -264,6 +293,16 @@ export class HttpClient { return text; } + private responseHeaders(response: Response): Record { + const headers: Record = {}; + if (typeof response.headers.forEach === "function") { + response.headers.forEach((value, key) => { + headers[key] = value; + }); + } + return headers; + } + /** * Build full URL with query parameters. * Automatically prefixes paths with the configured version prefix. diff --git a/packages/core/src/client/SapiomClient.test.ts b/packages/core/src/client/SapiomClient.test.ts index e44cd34cf..8e44e7d78 100644 --- a/packages/core/src/client/SapiomClient.test.ts +++ b/packages/core/src/client/SapiomClient.test.ts @@ -279,6 +279,40 @@ describe("SapiomClient", () => { ); }); + it("should preserve structured HTTP error details", async () => { + const errorData = { error: "Payment Required", transactionId: "tx-123" }; + mockFetch.mockResolvedValueOnce({ + ok: false, + status: 402, + statusText: "Payment Required", + headers: new Headers({ + "content-type": "application/json", + "x-sapiom-transaction-id": "tx-123", + }), + json: jest.fn().mockResolvedValue(errorData), + text: jest.fn().mockResolvedValue(JSON.stringify(errorData)), + }); + + await expect(client.request({ url: "/paid" })).rejects.toMatchObject({ + name: "HttpRequestError", + message: expect.stringMatching(/Request failed with status 402/), + status: 402, + statusText: "Payment Required", + data: errorData, + headers: expect.objectContaining({ + "x-sapiom-transaction-id": "tx-123", + }), + response: expect.objectContaining({ + status: 402, + statusText: "Payment Required", + data: errorData, + headers: expect.objectContaining({ + "x-sapiom-transaction-id": "tx-123", + }), + }), + }); + }); + it("should handle error responses with text content", async () => { const errorText = "Internal Server Error"; mockFetch.mockResolvedValueOnce({ From 7906db2201cf20957013d036e9f66cbba9f6e2fa Mon Sep 17 00:00:00 2001 From: burak33bb Date: Sun, 16 Aug 2026 23:49:55 +0300 Subject: [PATCH 2/2] chore(core): add changeset for HTTP error details --- .changeset/preserve-http-error-details.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/preserve-http-error-details.md diff --git a/.changeset/preserve-http-error-details.md b/.changeset/preserve-http-error-details.md new file mode 100644 index 000000000..97c4faa65 --- /dev/null +++ b/.changeset/preserve-http-error-details.md @@ -0,0 +1,5 @@ +--- +"@sapiom/core": patch +--- + +Preserve structured HTTP error details on core client request failures.