Files
bun.sh/test/js/web/fetch/response.test.ts
SUZUKI Sosuke f4b6396eac Fix unhandled exception in JSC__JSPromise__wrap when resolving promise (#23961)
### What does this PR do?

Previously, `JSC__JSPromise__wrap` would call
`JSC::JSPromise::resolvedPromise(globalObject, result)` without checking
if an exception was thrown during promise resolution. This
could happen in certain edge cases, such as when the result value is a
thenable that triggers stack overflow, or when the promise resolution
mechanism itself encounters an error.
When such exceptions occurred, they would escape back to the Zig code,
causing the CatchScope assertion to fail with "ASSERTION FAILED:
Unexpected exception observed on thread"
instead of being properly handled.

This PR adds an exception check immediately after calling
`JSC::JSPromise::resolvedPromise()` and before the `RELEASE_AND_RETURN`
macro. If an exception is detected, the function
now clears it and returns a rejected promise with the exception value,
ensuring consistent error handling behavior. This matches the pattern
already used earlier in the function
for the initial function call exception handling.

### How did you verify your code works?

new and existing tests

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-10-24 21:36:33 -07:00

126 lines
4.0 KiB
TypeScript

import { describe, expect, test } from "bun:test";
import { normalizeBunSnapshot } from "harness";
test("zero args returns an otherwise empty 200 response", () => {
const response = new Response();
expect(response.status).toBe(200);
expect(response.statusText).toBe("");
});
test("calling cancel() on response body doesn't throw", () => {
expect(() => new Response("").body?.cancel()).not.toThrow();
});
test("undefined args don't throw", () => {
const response = new Response("", {
status: undefined,
statusText: undefined,
headers: undefined,
});
expect(response.status).toBe(200);
expect(response.statusText).toBe("");
});
test("1-arg form returns a 200 response", () => {
const response = new Response("body text");
expect(response.status).toBe(200);
expect(response.statusText).toBe("");
});
describe("2-arg form", () => {
test("can fill in status/statusText, and it works", () => {
const response = new Response("body text", {
status: 202,
statusText: "Accepted.",
});
expect(response.status).toBe(202);
expect(response.statusText).toBe("Accepted.");
});
test('empty object continues to return 200/""', () => {
const response = new Response("body text", {});
expect(response.status).toBe(200);
expect(response.statusText).toBe("");
});
});
test("print size", () => {
expect(normalizeBunSnapshot(Bun.inspect(new Response(Bun.file(import.meta.filename)))), import.meta.dir)
.toMatchInlineSnapshot(`
"Response (4.15 KB) {
ok: true,
url: "",
status: 200,
statusText: "",
headers: Headers {
"content-type": "text/javascript;charset=utf-8",
},
redirected: false,
bodyUsed: false,
FileRef ("<cwd>/test/js/web/fetch/response.test.ts") {
type: "text/javascript;charset=utf-8"
}
}"
`);
});
test("Response.redirect with invalid arguments should not crash", () => {
// This should not crash - issue #18414
// Passing a number as URL and string as init should handle gracefully
expect(() => Response.redirect(400, "a")).not.toThrow();
// Test various invalid argument combinations - should not crash
expect(() => Response.redirect(42, "test")).not.toThrow();
expect(() => Response.redirect(true, "string")).not.toThrow();
expect(() => Response.redirect(null, "init")).not.toThrow();
expect(() => Response.redirect(undefined, "value")).not.toThrow();
});
test("Response.redirect status code validation", () => {
// Valid redirect status codes should work
expect(() => Response.redirect("url", 301)).not.toThrow();
expect(() => Response.redirect("url", 302)).not.toThrow();
expect(() => Response.redirect("url", 303)).not.toThrow();
expect(() => Response.redirect("url", 307)).not.toThrow();
expect(() => Response.redirect("url", 308)).not.toThrow();
// Invalid status codes should throw RangeError
expect(() => Response.redirect("url", 200)).toThrow(RangeError);
expect(() => Response.redirect("url", 400)).toThrow(RangeError);
expect(() => Response.redirect("url", 500)).toThrow(RangeError);
// Status in object should also be validated
expect(() => Response.redirect("url", { status: 307 })).not.toThrow();
expect(() => Response.redirect("url", { status: 400 })).toThrow(RangeError);
// Check that the correct status is set
expect(Response.redirect("url", 301).status).toBe(301);
expect(Response.redirect("url", { status: 308 }).status).toBe(308);
});
test("new Response(123, { statusText: 123 }) does not throw", () => {
// @ts-expect-error
expect(new Response("123", { statusText: 123 }).statusText).toBe("123");
});
test("new Response(123, { method: 456 }) does not throw", () => {
// @ts-expect-error
expect(() => new Response("123", { method: 456 })).not.toThrow();
});
test("handle stack overflow", () => {
function f0(a1, a2) {
const v4 = new Response();
// @ts-ignore
const v5 = v4.text(a2, a2, v4, f0, f0);
a1(a1); // Recursive call causes stack overflow
return v5;
}
expect(() => {
// @ts-ignore
f0(f0);
}).toThrow("Maximum call stack size exceeded.");
});