Skip to content

Commit 4a32c1b

Browse files
committed
fix(response): absorb errors thrown in onResponse hook
1 parent eb30d5f commit 4a32c1b

3 files changed

Lines changed: 38 additions & 9 deletions

File tree

src/response.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,22 @@ export function toResponse(
2828

2929
const { onResponse } = config;
3030
if (onResponse) {
31-
return Promise.resolve(onResponse(response as Response, event)).then(
32-
() =>
33-
((event as any)[kEventDispose] as DisposeState | undefined)?.observe(
34-
response as Response,
35-
val,
36-
) ?? (response as Response),
37-
);
31+
// onResponse is a terminal side-effect hook (returns void). A throw/rejection here must not
32+
// escape the lifecycle (onError already ran); absorb and log it (consistent with dispose
33+
// callbacks), then still return the already-built response. The hook is invoked inside `.then`
34+
// so a synchronous throw is caught too (not just a rejected promise).
35+
return Promise.resolve()
36+
.then(() => onResponse(response as Response, event))
37+
.catch((error) => {
38+
if (!config.silent) console.error(error);
39+
})
40+
.then(
41+
() =>
42+
((event as any)[kEventDispose] as DisposeState | undefined)?.observe(
43+
response as Response,
44+
val,
45+
) ?? (response as Response),
46+
);
3847
}
3948
return (
4049
((event as any)[kEventDispose] as DisposeState | undefined)?.observe(

test/bench/bundle.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ describe("benchmark", () => {
1818
if (process.env.DEBUG) {
1919
console.log(`Bundle size (H3): ${bundle.bytes} (gzip: ${bundle.gzipSize})`);
2020
}
21-
expect(bundle.bytes).toBeLessThanOrEqual(18_350); // <18.35kb
21+
expect(bundle.bytes).toBeLessThanOrEqual(18_400); // <18.4kb
2222
expect(bundle.gzipSize).toBeLessThanOrEqual(6_950); // <6.95kb
2323
});
2424

@@ -34,7 +34,7 @@ describe("benchmark", () => {
3434
if (process.env.DEBUG) {
3535
console.log(`Bundle size (H3Core): ${bundle.bytes} (gzip: ${bundle.gzipSize})`);
3636
}
37-
expect(bundle.bytes).toBeLessThanOrEqual(7550); // <7.55kb
37+
expect(bundle.bytes).toBeLessThanOrEqual(7600); // <7.6kb
3838
expect(bundle.gzipSize).toBeLessThanOrEqual(3000); // <3kb
3939
});
4040

test/hooks.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,26 @@ describeMatrix("hooks", (t, { it, expect }) => {
5353
expect(t.hooks.onResponse).toHaveBeenCalledTimes(1);
5454
});
5555

56+
it("absorbs a throwing onResponse hook without failing the request", async () => {
57+
t.app.use(() => "Hello World!");
58+
const hookError = new Error("onResponse boom");
59+
t.hooks.onResponse.mockImplementationOnce(() => {
60+
throw hookError;
61+
});
62+
const consoleError = vi.spyOn(console, "error").mockImplementation(() => {});
63+
64+
const res = await t.fetch("/foo");
65+
66+
// Request still succeeds with the already-built response
67+
expect(res.status).toBe(200);
68+
expect(await res.text()).toBe("Hello World!");
69+
70+
// Error is absorbed and logged, not routed back into onError
71+
expect(t.hooks.onError).toHaveBeenCalledTimes(0);
72+
expect(consoleError).toHaveBeenCalledWith(hookError);
73+
consoleError.mockRestore();
74+
});
75+
5676
it("calls onRequest and onResponse when an unhandled error occurs", async () => {
5777
t.app.use((event) => {
5878
// @ts-expect-error

0 commit comments

Comments
 (0)