Skip to content

Commit 45c1f0d

Browse files
authored
fix: surface HTTP errors when using http requests (#389)
* fix: surface HTTP errors when using http requests
1 parent 96830c5 commit 45c1f0d

File tree

2 files changed

+71
-1
lines changed

2 files changed

+71
-1
lines changed

typescript-sdk/packages/client/src/run/__tests__/http-request.test.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ describe("runHttpRequest", () => {
3434
mockHeaders.append("Content-Type", "application/json");
3535

3636
const mockResponse = {
37+
ok: true,
3738
status: 200,
3839
headers: mockHeaders,
3940
body: {
@@ -90,6 +91,7 @@ describe("runHttpRequest", () => {
9091
mockHeaders.append("Content-Type", "application/json");
9192

9293
const mockResponse = {
94+
ok: true,
9395
status: 200,
9496
headers: mockHeaders,
9597
body: {
@@ -142,6 +144,7 @@ describe("runHttpRequest", () => {
142144
mockHeaders.append("Content-Type", "application/json");
143145

144146
const mockResponse = {
147+
ok: true,
145148
status: 200,
146149
headers: mockHeaders,
147150
body: {
@@ -192,4 +195,53 @@ describe("runHttpRequest", () => {
192195
// Clean up
193196
subscription.unsubscribe();
194197
});
198+
199+
it("should throw HTTP error on occurs", async () => {
200+
// Mock a 404 error response with JSON body
201+
const mockHeaders = new Headers();
202+
mockHeaders.append("content-type", "application/json");
203+
204+
const mockText = '{"message":"User not found"}';
205+
206+
const mockResponse = {
207+
ok: false,
208+
status: 404,
209+
headers: mockHeaders,
210+
// our error-path reads .text() (not streaming)
211+
text: jest.fn().mockResolvedValue(mockText),
212+
} as unknown as Response;
213+
214+
// Override fetch for this test
215+
fetchMock.mockResolvedValue(mockResponse);
216+
217+
const observable = runHttpRequest("https://example.com/api", { method: "GET" });
218+
219+
const nextSpy = jest.fn();
220+
221+
await new Promise<void>((resolve) => {
222+
const sub = observable.subscribe({
223+
next: nextSpy,
224+
error: (err: any) => {
225+
// error should carry status + parsed payload
226+
expect(err).toBeInstanceOf(Error);
227+
expect(err.status).toBe(404);
228+
expect(err.payload).toEqual({ message: "User not found" });
229+
// readable message is okay too (optional)
230+
expect(err.message).toContain("HTTP 404");
231+
expect(err.message).toContain("User not found");
232+
resolve();
233+
sub.unsubscribe();
234+
},
235+
complete: () => {
236+
fail("Should not complete on HTTP error");
237+
},
238+
});
239+
});
240+
241+
// Should not have emitted any data events on error short-circuit
242+
expect(nextSpy).not.toHaveBeenCalled();
243+
244+
// Ensure we read the error body exactly once
245+
expect((mockResponse as any).text).toHaveBeenCalledTimes(1);
246+
});
195247
});

typescript-sdk/packages/client/src/run/http-request.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Observable, from, defer, throwError } from "rxjs";
2-
import { switchMap } from "rxjs/operators";
2+
import { mergeMap, switchMap } from "rxjs/operators";
33

44
export enum HttpEventType {
55
HEADERS = "headers",
@@ -23,6 +23,24 @@ export const runHttpRequest = (url: string, requestInit: RequestInit): Observabl
2323
// Defer the fetch so that it's executed when subscribed to
2424
return defer(() => from(fetch(url, requestInit))).pipe(
2525
switchMap((response) => {
26+
if (!response.ok) {
27+
const contentType = response.headers.get("content-type") || "";
28+
// Read the (small) error body once, then error the stream
29+
return from(response.text()).pipe(
30+
mergeMap((text) => {
31+
let payload: unknown = text;
32+
if (contentType.includes("application/json")) {
33+
try { payload = JSON.parse(text); } catch {/* keep raw text */}
34+
}
35+
const err: any = new Error(
36+
`HTTP ${response.status}: ${typeof payload === "string" ? payload : JSON.stringify(payload)}`
37+
);
38+
err.status = response.status;
39+
err.payload = payload;
40+
return throwError(() => err);
41+
})
42+
);
43+
}
2644
// Emit headers event first
2745
const headersEvent: HttpHeadersEvent = {
2846
type: HttpEventType.HEADERS,

0 commit comments

Comments
 (0)