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",