From 862cf1adecdb6a3b7eaa0145b6bf80add1a68c96 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Tue, 29 Sep 2026 01:53:32 +0000 Subject: [PATCH 1/2] bake: Response() of bun:app without new no longer writes through a null pointer The call form of the bun:app Response handed a null out-pointer for the JSX flag to the Rust constructor. The constructor wrote through it for every object body. The constructor and the call form now share one C++ function. It finds out if the body is a JSX element and passes the answer to Rust by value. Response() wraps the element as new Response() does. --- src/jsc/JSValue.rs | 9 --- src/jsc/bindings/JSBakeResponse.cpp | 81 ++++++++----------- src/runtime/webcore/BakeResponse.rs | 32 +++----- test/bake/dev/react-response.test.ts | 26 ++++++ .../dev/response-to-bake-response.test.ts | 38 +++++++++ 5 files changed, 110 insertions(+), 76 deletions(-) diff --git a/src/jsc/JSValue.rs b/src/jsc/JSValue.rs index 1eb5cfd82bfa..654176e8ec0b 100644 --- a/src/jsc/JSValue.rs +++ b/src/jsc/JSValue.rs @@ -363,15 +363,6 @@ impl JSValue { pub fn is_error(self) -> bool { self.is_cell() && self.js_type() == JSType::ErrorInstance } - /// `JSValue.isJSXElement(globalObject)`. Checks via the - /// global's `Symbol.for("react.element")` / `Symbol.for("react.transitional.element")` - /// for `$$typeof`; may invoke a user getter and throw. - pub fn is_jsx_element(self, global: &JSGlobalObject) -> JsResult { - unsafe extern "C" { - safe fn JSC__JSValue__isJSXElement(this: JSValue, global: &JSGlobalObject) -> bool; - } - host_fn::from_js_host_call_generic(global, || JSC__JSValue__isJSXElement(self, global)) - } /// `JSValue.isAggregateError(globalObject)`. #[inline] pub fn is_aggregate_error(self, global: &JSGlobalObject) -> bool { diff --git a/src/jsc/bindings/JSBakeResponse.cpp b/src/jsc/bindings/JSBakeResponse.cpp index 226dce1e2411..4e1bd001c483 100644 --- a/src/jsc/bindings/JSBakeResponse.cpp +++ b/src/jsc/bindings/JSBakeResponse.cpp @@ -23,14 +23,14 @@ namespace Bun { -extern JSC_CALLCONV void* JSC_HOST_CALL_ATTRIBUTES BakeResponseClass__constructForSSR(JSC::JSGlobalObject*, JSC::CallFrame*, int*, JSC::EncodedJSValue); +extern JSC_CALLCONV void* JSC_HOST_CALL_ATTRIBUTES BakeResponseClass__constructForSSR(JSC::JSGlobalObject*, JSC::CallFrame*, bool isJSX, JSC::EncodedJSValue); extern "C" SYSV_ABI JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES ResponseClass__constructError(JSC::JSGlobalObject*, JSC::CallFrame*); extern "C" SYSV_ABI JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES ResponseClass__constructJSON(JSC::JSGlobalObject*, JSC::CallFrame*); extern "C" SYSV_ABI JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES BakeResponseClass__constructRender(JSC::JSGlobalObject*, JSC::CallFrame*); extern "C" SYSV_ABI JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES BakeResponseClass__constructRedirect(JSC::JSGlobalObject*, JSC::CallFrame*); extern JSC_CALLCONV size_t Response__estimatedSize(void* ptr); -bool isJSXElement(JSC::EncodedJSValue JSValue0, JSC::JSGlobalObject* globalObject) +static bool isJSXElement(JSC::EncodedJSValue JSValue0, JSC::JSGlobalObject* globalObject) { auto* zigGlobal = static_cast(globalObject); @@ -61,11 +61,6 @@ bool isJSXElement(JSC::EncodedJSValue JSValue0, JSC::JSGlobalObject* globalObjec return false; } -extern "C" bool JSC__JSValue__isJSXElement(JSC::EncodedJSValue JSValue0, JSC::JSGlobalObject* globalObject) -{ - return isJSXElement(JSValue0, globalObject); -} - extern JSC_CALLCONV JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES BakeResponse__createForSSR(Zig::GlobalObject* globalObject, void* ptr, uint8_t kind) { Structure* structure = globalObject->bakeAdditions().JSBakeResponseStructure(globalObject); @@ -159,6 +154,36 @@ void JSBakeResponse::visitChildrenImpl(JSCell* cell, Visitor& visitor) DEFINE_VISIT_CHILDREN(JSBakeResponse); +static ALWAYS_INLINE JSC::EncodedJSValue constructBakeResponse(Zig::GlobalObject* globalObject, JSC::CallFrame* callFrame, JSC::Structure* structure) +{ + JSC::VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSValue body = callFrame->argument(0); + bool isJSX = isJSXElement(JSValue::encode(body), globalObject); + RETURN_IF_EXCEPTION(scope, {}); + + JSBakeResponse* instance = JSBakeResponse::create(vm, globalObject, structure, nullptr); + + void* ptr = BakeResponseClass__constructForSSR(globalObject, callFrame, isJSX, JSValue::encode(instance)); + if (scope.exception()) [[unlikely]] { + ASSERT_WITH_MESSAGE(!ptr, "Memory leak detected: new Response() allocated memory without checking for exceptions."); + return JSValue::encode(JSC::jsUndefined()); + } + + instance->m_ctx = ptr; + + if (isJSX) { + instance->wrapInnerComponent(globalObject, vm, body, callFrame->argument(1)); + RETURN_IF_EXCEPTION(scope, {}); + } + + auto size = Response__estimatedSize(ptr); + vm.heap.reportExtraMemoryAllocated(instance, size); + + RELEASE_AND_RETURN(scope, JSValue::encode(instance)); +} + class JSBakeResponseConstructor final : public JSC::InternalFunction { public: using Base = JSC::InternalFunction; @@ -192,51 +217,13 @@ class JSBakeResponseConstructor final : public JSC::InternalFunction { RETURN_IF_EXCEPTION(scope, {}); } - JSBakeResponse* instance = JSBakeResponse::create(vm, globalObject, structure, nullptr); - - int arg_was_jsx = 0; - void* ptr = BakeResponseClass__constructForSSR(globalObject, callFrame, &arg_was_jsx, JSValue::encode(instance)); - if (scope.exception()) [[unlikely]] { - ASSERT_WITH_MESSAGE(!ptr, "Memory leak detected: new Response() allocated memory without checking for exceptions."); - return JSValue::encode(JSC::jsUndefined()); - } - - instance->m_ctx = ptr; - - if (arg_was_jsx == 1 && callFrame->argumentCount() > 0) { - JSValue arg = callFrame->argument(0); - JSValue responseOptions = callFrame->argumentCount() > 1 ? callFrame->argument(1) : JSC::jsUndefined(); - instance->wrapInnerComponent(globalObject, vm, arg, responseOptions); - } - - auto size = Response__estimatedSize(ptr); - vm.heap.reportExtraMemoryAllocated(instance, size); - - auto value = JSValue::encode(instance); - RELEASE_AND_RETURN(scope, value); + RELEASE_AND_RETURN(scope, constructBakeResponse(globalObject, callFrame, structure)); } static JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES call(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame) { Zig::GlobalObject* globalObject = static_cast(lexicalGlobalObject); - JSC::VM& vm = globalObject->vm(); - auto scope = DECLARE_THROW_SCOPE(vm); - - Structure* structure = globalObject->bakeAdditions().JSBakeResponseStructure(globalObject); - JSBakeResponse* instance = JSBakeResponse::create(vm, globalObject, structure, nullptr); - - void* ptr = BakeResponseClass__constructForSSR(globalObject, callFrame, nullptr, JSValue::encode(instance)); - if (scope.exception()) [[unlikely]] { - ASSERT_WITH_MESSAGE(!ptr, "Memory leak detected: new Response() allocated memory without checking for exceptions."); - return JSValue::encode(JSC::jsUndefined()); - } - - instance->m_ctx = ptr; - - auto size = Response__estimatedSize(ptr); - vm.heap.reportExtraMemoryAllocated(instance, size); - - RELEASE_AND_RETURN(scope, JSValue::encode(instance)); + return constructBakeResponse(globalObject, callFrame, globalObject->bakeAdditions().JSBakeResponseStructure(globalObject)); } template diff --git a/src/runtime/webcore/BakeResponse.rs b/src/runtime/webcore/BakeResponse.rs index 3e88bf4a3bd5..382a95193ed8 100644 --- a/src/runtime/webcore/BakeResponse.rs +++ b/src/runtime/webcore/BakeResponse.rs @@ -1,4 +1,4 @@ -use core::ffi::{c_int, c_void}; +use core::ffi::c_void; use crate::webcore::Response; use crate::webcore::response::{HeadersRef, Init}; @@ -61,12 +61,10 @@ bun_jsc::jsc_host_abi! { pub(crate) unsafe fn BakeResponseClass__constructForSSR( global_object: &JSGlobalObject, call_frame: &CallFrame, - bake_ssr_has_jsx: *mut c_int, + is_jsx: bool, js_this: JSValue, ) -> *mut c_void { - // SAFETY: caller (C++) guarantees `bake_ssr_has_jsx` is a valid, exclusive out-pointer for the call. - let bake_ssr_has_jsx = unsafe { &mut *bake_ssr_has_jsx }; - match constructor(global_object, call_frame, bake_ssr_has_jsx, js_this) { + match constructor(global_object, call_frame, is_jsx, js_this) { Ok(response) => response.cast::(), Err(JsError::Thrown) => core::ptr::null_mut(), Err(JsError::Terminated) => { @@ -85,25 +83,19 @@ bun_jsc::jsc_host_abi! { fn constructor( global_this: &JSGlobalObject, callframe: &CallFrame, - bake_ssr_has_jsx: &mut c_int, + is_jsx: bool, js_this: JSValue, ) -> JsResult<*mut Response> { - let arguments: [JSValue; 2] = callframe.arguments_as_array::<2>(); - // Allow `return new Response( ... , { ... }` // inside of a react component - if !arguments[0].is_undefined_or_null() && arguments[0].is_object() { - *bake_ssr_has_jsx = 0; - if arguments[0].is_jsx_element(global_this)? { - let vm = global_this.bun_vm().as_mut(); - if let Some(async_local_storage) = vm.get_dev_server_async_local_storage()? { - assert_streaming_disabled( - global_this, - async_local_storage, - b"new Response(, { ... })", - )?; - } - *bake_ssr_has_jsx = 1; + if is_jsx { + let vm = global_this.bun_vm().as_mut(); + if let Some(async_local_storage) = vm.get_dev_server_async_local_storage()? { + assert_streaming_disabled( + global_this, + async_local_storage, + b"new Response(, { ... })", + )?; } } diff --git a/test/bake/dev/react-response.test.ts b/test/bake/dev/react-response.test.ts index 1c8a5c91e0b8..ebb52c3d37ae 100644 --- a/test/bake/dev/react-response.test.ts +++ b/test/bake/dev/react-response.test.ts @@ -105,6 +105,32 @@ devTest("new Response with JSX and custom headers", { }, }); +devTest("Response with JSX called without new", { + framework: "react", + files: { + "pages/index.tsx": ` + export const streaming = false; + export const mode = "ssr"; + + export default async function IndexPage() { + return Response(

Hello World

, { + status: 201, + headers: { + "X-Custom-Header": "test-value" + } + }); + } + `, + }, + async test(dev) { + const response = await dev.fetch("/"); + expect(response.status).toBe(201); + expect(response.headers.get("X-Custom-Header")).toBe("test-value"); + const text = await response.text(); + expect(text).toContain("

Hello World

"); + }, +}); + // Test case 5: new Response with JSX when streaming = true (should error) devTest("new Response with JSX when streaming = true should error", { framework: "react", diff --git a/test/bake/dev/response-to-bake-response.test.ts b/test/bake/dev/response-to-bake-response.test.ts index 7e99be1236da..c9531f854fa6 100644 --- a/test/bake/dev/response-to-bake-response.test.ts +++ b/test/bake/dev/response-to-bake-response.test.ts @@ -290,4 +290,42 @@ describe.concurrent('the Response of "bun:app" outside the dev server', () => { `); expect(result).toEqual({ stdout: "201 true\n", stderr: "", exitCode: 0 }); }); + + test("Response() without new returns a Response", async () => { + const result = await run(` + for (const body of ["text", { a: 1 }]) { + const response = Response(body, { status: 201 }); + console.log(response instanceof Response, response.status, await response.text()); + } + `); + expect(result).toEqual({ stdout: "true 201 text\ntrue 201 [object Object]\n", stderr: "", exitCode: 0 }); + }); + + test("Response() without new returns the element as a component", async () => { + const result = await run(` + const element = { $$typeof: Symbol.for("react.transitional.element"), type: "div", key: null, props: {} }; + const response = Response(element, { status: 201 }); + console.log(response instanceof Response, response.status, response.type() === element); + `); + expect(result).toEqual({ stdout: "true 201 true\n", stderr: "", exitCode: 0 }); + }); + + test("an error from the $$typeof getter of the body propagates", async () => { + const result = await run(` + const body = { + get $$typeof() { + throw new Error("from the getter"); + }, + }; + for (const construct of [() => new Response(body), () => Response(body)]) { + try { + construct(); + console.log("no error"); + } catch (error) { + console.log(error.message); + } + } + `); + expect(result).toEqual({ stdout: "from the getter\nfrom the getter\n", stderr: "", exitCode: 0 }); + }); }); From 337f3fe0e3b6df302409f0752b48adb2f7ddb014 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Tue, 29 Sep 2026 02:06:07 +0000 Subject: [PATCH 2/2] test: the call form of the bun:app Response in a streaming route --- test/bake/dev/react-response.test.ts | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/test/bake/dev/react-response.test.ts b/test/bake/dev/react-response.test.ts index ebb52c3d37ae..f0447438f9d4 100644 --- a/test/bake/dev/react-response.test.ts +++ b/test/bake/dev/react-response.test.ts @@ -159,6 +159,33 @@ devTest("new Response with JSX when streaming = true should error", { }, }); +devTest("Response with JSX called without new when streaming = true should error", { + framework: "react", + files: { + "pages/index.tsx": ` + export const streaming = true; + export const mode = "ssr"; + + export default async function IndexPage() { + return Response(

Hello World

, { + status: 201, + headers: { + "X-Custom-Header": "test-value" + } + }); + } + `, + }, + async test(dev) { + const response = await dev.fetch("/"); + const text = await response.text(); + const fallback_message_container = getFallbackMessageContainer(text); + expect(fallback_message_container.problems?.exceptions[0].message).toContain( + '"new Response(, { ... })" is not available when `export const streaming = true`', + ); + }, +}); + // Test case 6: Response.redirect() - content matching devTest("Response.redirect() - content matching", { framework: "react",