Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
34af2fc628 Fix exception handling in Response/Request body methods and JSPromise.wrap
This commit fixes several issues related to exception handling in async body methods
(text, json, arrayBuffer, bytes) and the generated C++ wrappers.

Changes:
1. Body.zig: Handle JSTerminated errors from JSPromise.wrap by catching and returning .zero
   - getText, getJSON, getArrayBuffer, getBytes now properly handle termination exceptions

2. generate-classes.ts: Fix scope management in generated C++ host function wrappers
   - Replace DECLARE_CATCH_SCOPE with scope.exception() to avoid creating temporary scopes
   - Use RELEASE_AND_RETURN instead of plain return to properly release ThrowScope

3. JSPromise.zig: Clear exceptions before CatchScope destruction
   - Prevents assertion failures when exceptions occur during stack unwinding

4. Add regression test for Response.text() with stack overflow scenarios

These changes ensure that exceptions (especially stack overflow from recursion) are
properly handled and don't trigger assertion failures in debug builds.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-22 08:36:22 +00:00
4 changed files with 59 additions and 8 deletions

View File

@@ -195,7 +195,11 @@ pub const JSPromise = opaque {
defer scope.deinit();
var ctx = Wrapper{ .args = args };
const promise = JSC__JSPromise__wrap(globalObject, &ctx, @ptrCast(&Wrapper.call));
try scope.assertNoExceptionExceptTermination();
scope.assertNoExceptionExceptTermination() catch |err| {
// Clear the exception before the scope is destroyed to avoid assertion failures
scope.clearException();
return err;
};
return promise;
}

View File

@@ -1118,7 +1118,7 @@ pub fn Mixin(comptime Type: type) type {
}
var blob = value.useAsAnyBlobAllowNonUTF8String();
return jsc.JSPromise.wrap(globalObject, lifetimeWrap(AnyBlob.toString, .transfer), .{ &blob, globalObject });
return jsc.JSPromise.wrap(globalObject, lifetimeWrap(AnyBlob.toString, .transfer), .{ &blob, globalObject }) catch return .zero;
}
pub fn getBody(this: *Type, globalThis: *jsc.JSGlobalObject) bun.JSError!JSValue {
@@ -1203,7 +1203,7 @@ pub fn Mixin(comptime Type: type) type {
var blob = value.useAsAnyBlobAllowNonUTF8String();
return jsc.JSPromise.wrap(globalObject, lifetimeWrap(AnyBlob.toJSON, .share), .{ &blob, globalObject });
return jsc.JSPromise.wrap(globalObject, lifetimeWrap(AnyBlob.toJSON, .share), .{ &blob, globalObject }) catch return .zero;
}
fn handleBodyAlreadyUsed(globalObject: *jsc.JSGlobalObject) JSValue {
@@ -1245,7 +1245,7 @@ pub fn Mixin(comptime Type: type) type {
// toArrayBuffer in AnyBlob checks for non-UTF8 strings
var blob: AnyBlob = value.useAsAnyBlobAllowNonUTF8String();
return jsc.JSPromise.wrap(globalObject, lifetimeWrap(AnyBlob.toArrayBuffer, .transfer), .{ &blob, globalObject });
return jsc.JSPromise.wrap(globalObject, lifetimeWrap(AnyBlob.toArrayBuffer, .transfer), .{ &blob, globalObject }) catch return .zero;
}
pub fn getBytes(this: *Type, globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!jsc.JSValue {
@@ -1280,7 +1280,7 @@ pub fn Mixin(comptime Type: type) type {
// toArrayBuffer in AnyBlob checks for non-UTF8 strings
var blob: AnyBlob = value.useAsAnyBlobAllowNonUTF8String();
return jsc.JSPromise.wrap(globalObject, lifetimeWrap(AnyBlob.toUint8Array, .transfer), .{ &blob, globalObject });
return jsc.JSPromise.wrap(globalObject, lifetimeWrap(AnyBlob.toUint8Array, .transfer), .{ &blob, globalObject }) catch return .zero;
}
pub fn getFormData(this: *Type, globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!jsc.JSValue {

View File

@@ -1295,7 +1295,7 @@ JSC_DEFINE_HOST_FUNCTION(${symbolName(typeName, name)}Callback, (JSGlobalObject
JSC::EncodedJSValue result = ${symbolName(typeName, fn)}(thisObject->wrapped(), lexicalGlobalObject, callFrame${proto[name].passThis ? ", JSValue::encode(thisObject)" : ""});
ASSERT_WITH_MESSAGE(!JSValue::decode(result).isEmpty() or DECLARE_CATCH_SCOPE(vm).exception() != 0, \"${typeName}.${proto[name].fn} returned an empty value without an exception\");
ASSERT_WITH_MESSAGE(!JSValue::decode(result).isEmpty() or scope.exception() != 0, \"${typeName}.${proto[name].fn} returned an empty value without an exception\");
${
!proto[name].DOMJIT
@@ -1323,10 +1323,10 @@ JSC_DEFINE_HOST_FUNCTION(${symbolName(typeName, name)}Callback, (JSGlobalObject
}
#endif
return result;
RELEASE_AND_RETURN(scope, result);
#endif
return ${symbolName(typeName, proto[name].fn)}(thisObject->wrapped(), lexicalGlobalObject, callFrame${proto[name].passThis ? ", JSValue::encode(thisObject)" : ""});
RELEASE_AND_RETURN(scope, ${symbolName(typeName, proto[name].fn)}(thisObject->wrapped(), lexicalGlobalObject, callFrame${proto[name].passThis ? ", JSValue::encode(thisObject)" : ""}));
}
`);

View File

@@ -0,0 +1,47 @@
// Test for: Response.text() causing assertion failure with stack overflow
// This test ensures that stack overflow exceptions during Response.text() calls
// are properly handled and don't trigger assertion failures in debug builds.
import { expect, test } from "bun:test";
test("Response.text() handles stack overflow exceptions correctly", () => {
function f0(a1: Function, a2?: any): Promise<string> {
const v4 = new Response();
const v5 = v4.text();
a1(a1); // Recursive call causes stack overflow
return v5;
}
// This should throw a RangeError for stack overflow, not crash with assertion
expect(() => f0(f0)).toThrow(RangeError);
});
test("Response.text() with moderate recursion works correctly", async () => {
let depth = 0;
const maxDepth = 100; // Moderate recursion that won't overflow
async function f0(a1: Function): Promise<string> {
depth++;
if (depth > maxDepth) {
throw new Error("Max depth reached");
}
const v4 = new Response("test content");
const v5 = v4.text();
try {
await f0(a1);
} catch (e: any) {
if (e.message === "Max depth reached") {
return v5;
}
throw e;
}
return v5;
}
const result = await f0(f0);
expect(result).toBe("test content");
expect(depth).toBe(maxDepth + 1);
});