From 457643b7826bdf2b8ced0e6fc594f742ad128ca1 Mon Sep 17 00:00:00 2001 From: ran Date: Thu, 18 Sep 2025 12:10:09 +0200 Subject: [PATCH 1/2] fix: surface HTTP errors when using http requests --- .../src/run/__tests__/http-request.test.ts | 49 +++++++++++++++++++ .../packages/client/src/run/http-request.ts | 20 +++++++- 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/typescript-sdk/packages/client/src/run/__tests__/http-request.test.ts b/typescript-sdk/packages/client/src/run/__tests__/http-request.test.ts index 1169bae8a..34a519474 100644 --- a/typescript-sdk/packages/client/src/run/__tests__/http-request.test.ts +++ b/typescript-sdk/packages/client/src/run/__tests__/http-request.test.ts @@ -192,4 +192,53 @@ describe("runHttpRequest", () => { // Clean up subscription.unsubscribe(); }); + + it("should throw HTTP error on occurs", async () => { + // Mock a 404 error response with JSON body + const mockHeaders = new Headers(); + mockHeaders.append("content-type", "application/json"); + + const mockText = '{"message":"User not found"}'; + + const mockResponse = { + ok: false, + status: 404, + headers: mockHeaders, + // our error-path reads .text() (not streaming) + text: jest.fn().mockResolvedValue(mockText), + } as unknown as Response; + + // Override fetch for this test + fetchMock.mockResolvedValue(mockResponse); + + const observable = runHttpRequest("https://example.com/api", { method: "GET" }); + + const nextSpy = jest.fn(); + + await new Promise((resolve) => { + const sub = observable.subscribe({ + next: nextSpy, + error: (err: any) => { + // error should carry status + parsed payload + expect(err).toBeInstanceOf(Error); + expect(err.status).toBe(404); + expect(err.payload).toEqual({ message: "User not found" }); + // readable message is okay too (optional) + expect(err.message).toContain("HTTP 404"); + expect(err.message).toContain("User not found"); + resolve(); + sub.unsubscribe(); + }, + complete: () => { + fail("Should not complete on HTTP error"); + }, + }); + }); + + // Should not have emitted any data events on error short-circuit + expect(nextSpy).not.toHaveBeenCalled(); + + // Ensure we read the error body exactly once + expect((mockResponse as any).text).toHaveBeenCalledTimes(1); + }); }); diff --git a/typescript-sdk/packages/client/src/run/http-request.ts b/typescript-sdk/packages/client/src/run/http-request.ts index cfba8659b..3e755a0be 100644 --- a/typescript-sdk/packages/client/src/run/http-request.ts +++ b/typescript-sdk/packages/client/src/run/http-request.ts @@ -1,5 +1,5 @@ import { Observable, from, defer, throwError } from "rxjs"; -import { switchMap } from "rxjs/operators"; +import { mergeMap, switchMap } from "rxjs/operators"; export enum HttpEventType { HEADERS = "headers", @@ -23,6 +23,24 @@ export const runHttpRequest = (url: string, requestInit: RequestInit): Observabl // Defer the fetch so that it's executed when subscribed to return defer(() => from(fetch(url, requestInit))).pipe( switchMap((response) => { + if (!response.ok) { + const contentType = response.headers.get("content-type") || ""; + // Read the (small) error body once, then error the stream + return from(response.text()).pipe( + mergeMap((text) => { + let payload: unknown = text; + if (contentType.includes("application/json")) { + try { payload = JSON.parse(text); } catch {/* keep raw text */} + } + const err: any = new Error( + `HTTP ${response.status}: ${typeof payload === "string" ? payload : JSON.stringify(payload)}` + ); + err.status = response.status; + err.payload = payload; + return throwError(() => err); + }) + ); + } // Emit headers event first const headersEvent: HttpHeadersEvent = { type: HttpEventType.HEADERS, From d43c45ae03b5cb81edaa6fb1bb8d6ecfbef90e7e Mon Sep 17 00:00:00 2001 From: ran Date: Thu, 18 Sep 2025 12:27:13 +0200 Subject: [PATCH 2/2] fix test --- .../packages/client/src/run/__tests__/http-request.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/typescript-sdk/packages/client/src/run/__tests__/http-request.test.ts b/typescript-sdk/packages/client/src/run/__tests__/http-request.test.ts index 34a519474..d3bf2fcbe 100644 --- a/typescript-sdk/packages/client/src/run/__tests__/http-request.test.ts +++ b/typescript-sdk/packages/client/src/run/__tests__/http-request.test.ts @@ -34,6 +34,7 @@ describe("runHttpRequest", () => { mockHeaders.append("Content-Type", "application/json"); const mockResponse = { + ok: true, status: 200, headers: mockHeaders, body: { @@ -90,6 +91,7 @@ describe("runHttpRequest", () => { mockHeaders.append("Content-Type", "application/json"); const mockResponse = { + ok: true, status: 200, headers: mockHeaders, body: { @@ -142,6 +144,7 @@ describe("runHttpRequest", () => { mockHeaders.append("Content-Type", "application/json"); const mockResponse = { + ok: true, status: 200, headers: mockHeaders, body: {