diff --git a/src/bun.js/bindings/NodeHTTP.cpp b/src/bun.js/bindings/NodeHTTP.cpp index a49e17adcd..2f5c810ebf 100644 --- a/src/bun.js/bindings/NodeHTTP.cpp +++ b/src/bun.js/bindings/NodeHTTP.cpp @@ -1,4 +1,5 @@ #include "root.h" +#include "JSDOMGlobalObjectInlines.h" #include "ZigGlobalObject.h" #include #include "helpers.h" @@ -259,10 +260,11 @@ JSC_DEFINE_HOST_FUNCTION(jsHTTPAssignHeaders, (JSGlobalObject * globalObject, Ca auto& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); - JSValue requestValue = callFrame->argument(0); - JSObject* objectValue = callFrame->argument(1).getObject(); - - JSC::InternalFieldTuple* tuple = JSC::InternalFieldTuple::create(vm, globalObject->m_internalFieldTupleStructure.get()); + // This is an internal binding. + JSValue requestValue = callFrame->uncheckedArgument(0); + JSObject* objectValue = callFrame->uncheckedArgument(1).getObject(); + JSC::InternalFieldTuple* tuple = jsCast(callFrame->uncheckedArgument(2)); + ASSERT(callFrame->argumentCount() == 3); JSValue headersValue = JSValue(); JSValue urlValue = JSValue(); @@ -409,13 +411,28 @@ JSValue createNodeHTTPInternalBinding(Zig::GlobalObject* globalObject) VM& vm = globalObject->vm(); obj->putDirect( vm, JSC::PropertyName(JSC::Identifier::fromString(vm, "setHeader"_s)), - JSC::JSFunction::create(vm, globalObject, 3, "setHeader"_s, jsHTTPSetHeader, ImplementationVisibility::Public), NoIntrinsic); + JSC::JSFunction::create(vm, globalObject, 3, "setHeader"_s, jsHTTPSetHeader, ImplementationVisibility::Public), 0); obj->putDirect( vm, JSC::PropertyName(JSC::Identifier::fromString(vm, "getHeader"_s)), - JSC::JSFunction::create(vm, globalObject, 2, "getHeader"_s, jsHTTPGetHeader, ImplementationVisibility::Public), NoIntrinsic); + JSC::JSFunction::create(vm, globalObject, 2, "getHeader"_s, jsHTTPGetHeader, ImplementationVisibility::Public), 0); obj->putDirect( vm, JSC::PropertyName(JSC::Identifier::fromString(vm, "assignHeaders"_s)), - JSC::JSFunction::create(vm, globalObject, 2, "assignHeaders"_s, jsHTTPAssignHeaders, ImplementationVisibility::Public), NoIntrinsic); + JSC::JSFunction::create(vm, globalObject, 2, "assignHeaders"_s, jsHTTPAssignHeaders, ImplementationVisibility::Public), 0); + obj->putDirect( + vm, JSC::PropertyName(JSC::Identifier::fromString(vm, "Response"_s)), + globalObject->JSResponseConstructor(), 0); + obj->putDirect( + vm, JSC::PropertyName(JSC::Identifier::fromString(vm, "Request"_s)), + globalObject->JSRequestConstructor(), 0); + obj->putDirect( + vm, JSC::PropertyName(JSC::Identifier::fromString(vm, "Blob"_s)), + globalObject->JSBlobConstructor(), 0); + obj->putDirect( + vm, JSC::PropertyName(JSC::Identifier::fromString(vm, "Headers"_s)), + WebCore::JSFetchHeaders::getConstructor(vm, globalObject), 0); + obj->putDirect( + vm, JSC::PropertyName(JSC::Identifier::fromString(vm, "headersTuple"_s)), + JSC::InternalFieldTuple::create(vm, globalObject->m_internalFieldTupleStructure.get()), 0); return obj; } diff --git a/src/js/node/http.ts b/src/js/node/http.ts index 87e1a47338..18c3c0a107 100644 --- a/src/js/node/http.ts +++ b/src/js/node/http.ts @@ -7,7 +7,21 @@ const { getHeader, setHeader, assignHeaders: assignHeadersFast, -} = $cpp("NodeHTTP.cpp", "createNodeHTTPInternalBinding"); + Response, + Request, + Headers, + Blob, + headersTuple, +} = $cpp("NodeHTTP.cpp", "createNodeHTTPInternalBinding") as { + getHeader: (headers: Headers, name: string) => string | undefined; + setHeader: (headers: Headers, name: string, value: string) => void; + assignHeaders: (object: any, req: Request, headersTuple: any) => boolean; + Response: (typeof globalThis)["Response"]; + Request: (typeof globalThis)["Request"]; + Headers: (typeof globalThis)["Headers"]; + Blob: (typeof globalThis)["Blob"]; + headersTuple: any; +}; const ObjectDefineProperty = Object.defineProperty; const ObjectSetPrototypeOf = Object.setPrototypeOf; @@ -664,10 +678,13 @@ function assignHeadersSlow(object, req) { function assignHeaders(object, req) { // This fast path is an 8% speedup for a "hello world" node:http server, and a 7% speedup for a "hello world" express server - const tuple = assignHeadersFast(req, object); - if (tuple !== null) { - object.headers = $getInternalField(tuple, 0); - object.rawHeaders = $getInternalField(tuple, 1); + if (assignHeadersFast(req, object, headersTuple)) { + const headers = $getInternalField(headersTuple, 0); + const rawHeaders = $getInternalField(headersTuple, 1); + $putInternalField(headersTuple, 0, undefined); + $putInternalField(headersTuple, 1, undefined); + object.headers = headers; + object.rawHeaders = rawHeaders; return true; } else { assignHeadersSlow(object, req); diff --git a/test/bun.lockb b/test/bun.lockb index b2294918f4..9508f9459a 100755 Binary files a/test/bun.lockb and b/test/bun.lockb differ diff --git a/test/js/node/http/node-http-primoridals.test.ts b/test/js/node/http/node-http-primoridals.test.ts new file mode 100644 index 0000000000..57b1a8506b --- /dev/null +++ b/test/js/node/http/node-http-primoridals.test.ts @@ -0,0 +1,118 @@ +import { test, expect } from "bun:test"; + +// This test passes by not hanging. +test("Overriding Request, Response, Headers, and Blob should not break node:http server", async () => { + const Response = globalThis.Response; + const Request = globalThis.Request; + const Headers = globalThis.Headers; + const Blob = globalThis.Blob; + + globalThis.Response = class MyResponse { + get body() { + throw new Error("body getter should not be called"); + } + + get headers() { + throw new Error("headers getter should not be called"); + } + + get status() { + throw new Error("status getter should not be called"); + } + + get statusText() { + throw new Error("statusText getter should not be called"); + } + + get ok() { + throw new Error("ok getter should not be called"); + } + + get url() { + throw new Error("url getter should not be called"); + } + + get type() { + throw new Error("type getter should not be called"); + } + }; + globalThis.Request = class MyRequest {}; + globalThis.Headers = class MyHeaders { + entries() { + throw new Error("entries should not be called"); + } + + get() { + throw new Error("get should not be called"); + } + + has() { + throw new Error("has should not be called"); + } + + keys() { + throw new Error("keys should not be called"); + } + + values() { + throw new Error("values should not be called"); + } + + forEach() { + throw new Error("forEach should not be called"); + } + + [Symbol.iterator]() { + throw new Error("[Symbol.iterator] should not be called"); + } + + [Symbol.toStringTag]() { + throw new Error("[Symbol.toStringTag] should not be called"); + } + + append() { + throw new Error("append should not be called"); + } + }; + globalThis.Blob = class MyBlob {}; + + const http = require("http"); + const server = http.createServer((req, res) => { + res.end("Hello World\n"); + }); + const { promise, resolve, reject } = Promise.withResolvers(); + + server.listen(0, () => { + const { port } = server.address(); + // client request + const req = http + .request(`http://localhost:${port}`, res => { + res + .on("data", data => { + expect(data.toString()).toBe("Hello World\n"); + }) + .on("end", () => { + server.close(); + console.log("closing time"); + }); + }) + .on("error", reject) + .end(); + }); + + server.on("close", () => { + resolve(); + }); + server.on("error", err => { + reject(err); + }); + + try { + await promise; + } finally { + globalThis.Response = Response; + globalThis.Request = Request; + globalThis.Headers = Headers; + globalThis.Blob = Blob; + } +}); diff --git a/test/js/third_party/remix/remix-build/server/index.js b/test/js/third_party/remix/remix-build/server/index.js new file mode 100644 index 0000000000..4e62ccc331 --- /dev/null +++ b/test/js/third_party/remix/remix-build/server/index.js @@ -0,0 +1,262 @@ +import { jsx, jsxs } from "react/jsx-runtime"; +import { PassThrough } from "node:stream"; +import { createReadableStreamFromReadable } from "@remix-run/node"; +import { RemixServer, Outlet, Meta, Links, ScrollRestoration, Scripts } from "@remix-run/react"; +import { isbot } from "isbot"; +import { renderToPipeableStream } from "react-dom/server"; +const ABORT_DELAY = 5e3; +function handleRequest(request, responseStatusCode, responseHeaders, remixContext, loadContext) { + return isbot(request.headers.get("user-agent") || "") + ? handleBotRequest(request, responseStatusCode, responseHeaders, remixContext) + : handleBrowserRequest(request, responseStatusCode, responseHeaders, remixContext); +} +function handleBotRequest(request, responseStatusCode, responseHeaders, remixContext) { + return new Promise((resolve, reject) => { + let shellRendered = false; + const { pipe, abort } = renderToPipeableStream( + /* @__PURE__ */ jsx(RemixServer, { + context: remixContext, + url: request.url, + abortDelay: ABORT_DELAY, + }), + { + onAllReady() { + shellRendered = true; + const body = new PassThrough(); + const stream = createReadableStreamFromReadable(body); + responseHeaders.set("Content-Type", "text/html"); + console.log(responseHeaders); + resolve( + new Response(stream, { + headers: responseHeaders, + status: responseStatusCode, + }), + ); + pipe(body); + }, + onShellError(error) { + reject(error); + }, + onError(error) { + responseStatusCode = 500; + if (shellRendered) { + console.error(error); + } + }, + }, + ); + setTimeout(abort, ABORT_DELAY); + }); +} +function handleBrowserRequest(request, responseStatusCode, responseHeaders, remixContext) { + return new Promise((resolve, reject) => { + let shellRendered = false; + const { pipe, abort } = renderToPipeableStream( + /* @__PURE__ */ jsx(RemixServer, { + context: remixContext, + url: request.url, + abortDelay: ABORT_DELAY, + }), + { + onShellReady() { + shellRendered = true; + const body = new PassThrough(); + const stream = createReadableStreamFromReadable(body); + responseHeaders.set("Content-Type", "text/html"); + resolve( + new Response(stream, { + headers: responseHeaders, + status: responseStatusCode, + }), + ); + pipe(body); + }, + onShellError(error) { + reject(error); + }, + onError(error) { + responseStatusCode = 500; + if (shellRendered) { + console.error(error); + } + }, + }, + ); + setTimeout(abort, ABORT_DELAY); + }); +} +const entryServer = /* @__PURE__ */ Object.freeze( + /* @__PURE__ */ Object.defineProperty( + { + __proto__: null, + default: handleRequest, + }, + Symbol.toStringTag, + { value: "Module" }, + ), +); +function Layout({ children }) { + return /* @__PURE__ */ jsxs("html", { + lang: "en", + children: [ + /* @__PURE__ */ jsxs("head", { + children: [ + /* @__PURE__ */ jsx("meta", { charSet: "utf-8" }), + /* @__PURE__ */ jsx("meta", { + name: "viewport", + content: "width=device-width, initial-scale=1", + }), + /* @__PURE__ */ jsx(Meta, {}), + /* @__PURE__ */ jsx(Links, {}), + ], + }), + /* @__PURE__ */ jsxs("body", { + children: [children, /* @__PURE__ */ jsx(ScrollRestoration, {}), /* @__PURE__ */ jsx(Scripts, {})], + }), + ], + }); +} +function App() { + return /* @__PURE__ */ jsx(Outlet, {}); +} +const route0 = /* @__PURE__ */ Object.freeze( + /* @__PURE__ */ Object.defineProperty( + { + __proto__: null, + Layout, + default: App, + }, + Symbol.toStringTag, + { value: "Module" }, + ), +); +const meta = () => { + return [{ title: "New Remix App" }, { name: "description", content: "Welcome to Remix!" }]; +}; +function Index() { + return /* @__PURE__ */ jsxs("div", { + className: "font-sans p-4", + children: [ + /* @__PURE__ */ jsx("h1", { + className: "text-3xl", + children: "Welcome to Remix", + }), + /* @__PURE__ */ jsxs("ul", { + className: "list-disc mt-4 pl-6 space-y-2", + children: [ + /* @__PURE__ */ jsx("li", { + children: /* @__PURE__ */ jsx("a", { + className: "text-blue-700 underline visited:text-purple-900", + target: "_blank", + href: "https://remix.run/start/quickstart", + rel: "noreferrer", + children: "5m Quick Start", + }), + }), + /* @__PURE__ */ jsx("li", { + children: /* @__PURE__ */ jsx("a", { + className: "text-blue-700 underline visited:text-purple-900", + target: "_blank", + href: "https://remix.run/start/tutorial", + rel: "noreferrer", + children: "30m Tutorial", + }), + }), + /* @__PURE__ */ jsx("li", { + children: /* @__PURE__ */ jsx("a", { + className: "text-blue-700 underline visited:text-purple-900", + target: "_blank", + href: "https://remix.run/docs", + rel: "noreferrer", + children: "Remix Docs", + }), + }), + ], + }), + ], + }); +} +const route1 = /* @__PURE__ */ Object.freeze( + /* @__PURE__ */ Object.defineProperty( + { + __proto__: null, + default: Index, + meta, + }, + Symbol.toStringTag, + { value: "Module" }, + ), +); +const serverManifest = { + entry: { + module: "/assets/entry.client-ER-smVHW.js", + imports: ["/assets/jsx-runtime-56DGgGmo.js", "/assets/components-BI_hnQlH.js"], + css: [], + }, + routes: { + root: { + id: "root", + parentId: void 0, + path: "", + index: void 0, + caseSensitive: void 0, + hasAction: false, + hasLoader: false, + hasClientAction: false, + hasClientLoader: false, + hasErrorBoundary: false, + module: "/assets/root-CBMuz_vA.js", + imports: ["/assets/jsx-runtime-56DGgGmo.js", "/assets/components-BI_hnQlH.js"], + css: ["/assets/root-BFUH26ow.css"], + }, + "routes/_index": { + id: "routes/_index", + parentId: "root", + path: void 0, + index: true, + caseSensitive: void 0, + hasAction: false, + hasLoader: false, + hasClientAction: false, + hasClientLoader: false, + hasErrorBoundary: false, + module: "/assets/_index-B6hwyHK-.js", + imports: ["/assets/jsx-runtime-56DGgGmo.js"], + css: [], + }, + }, + url: "/assets/manifest-c2e02a52.js", + version: "c2e02a52", +}; +const mode = "production"; +const assetsBuildDirectory = "build/client"; +const basename = "/"; +const future = { + v3_fetcherPersist: true, + v3_relativeSplatPath: true, + v3_throwAbortReason: true, + unstable_singleFetch: false, + unstable_fogOfWar: false, +}; +const isSpaMode = false; +const publicPath = "/"; +const entry = { module: entryServer }; +const routes = { + root: { + id: "root", + parentId: void 0, + path: "", + index: void 0, + caseSensitive: void 0, + module: route0, + }, + "routes/_index": { + id: "routes/_index", + parentId: "root", + path: void 0, + index: true, + caseSensitive: void 0, + module: route1, + }, +}; +export { serverManifest as assets, assetsBuildDirectory, basename, entry, future, isSpaMode, mode, publicPath, routes }; diff --git a/test/js/third_party/remix/remix.test.ts b/test/js/third_party/remix/remix.test.ts new file mode 100644 index 0000000000..16667e0f11 --- /dev/null +++ b/test/js/third_party/remix/remix.test.ts @@ -0,0 +1,44 @@ +import { describe, it, expect, test } from "bun:test"; +test("remix works", async () => { + process.env.PORT = "0"; + process.exitCode = 1; + process.env.NODE_ENV = "production"; + process.env.HOST = "localhost"; + process.argv = [process.argv[0], ".", require("path").join(__dirname, "remix-build", "server", "index.js")]; + const http = require("node:http"); + const originalListen = http.Server.prototype.listen; + let { promise, resolve, reject } = Promise.withResolvers(); + http.Server.prototype.listen = function listen(...args) { + setTimeout(() => { + resolve(this.address()); + }, 10); + return originalListen.apply(this, args); + }; + + require("@remix-run/serve/dist/cli.js"); + + // Wait long enough for the server's setTimeout to run. + await Bun.sleep(10); + + const port = (await promise).port; + + ({ promise, resolve, reject } = Promise.withResolvers()); + let chunks = []; + const req = http + .request(`http://localhost:${port}`, res => { + res + .on("data", data => { + chunks.push(data); + }) + .on("end", () => { + resolve(); + }) + .on("error", reject); + }) + .end(); + + await promise; + const data = Buffer.concat(chunks).toString(); + expect(data).toContain("Remix Docs"); + process.exitCode = 0; +}); diff --git a/test/package.json b/test/package.json index e0a9a275a3..96c4bd14c1 100644 --- a/test/package.json +++ b/test/package.json @@ -11,6 +11,8 @@ "@grpc/proto-loader": "0.7.10", "@napi-rs/canvas": "0.1.47", "@prisma/client": "5.8.0", + "@remix-run/react": "2.10.3", + "@remix-run/serve": "2.10.3", "@resvg/resvg-js": "2.4.1", "@swc/core": "1.3.38", "@types/ws": "8.5.10", @@ -23,6 +25,7 @@ "express": "4.18.2", "fast-glob": "3.3.1", "iconv-lite": "0.6.3", + "isbot": "5.1.13", "jest-extended": "4.0.0", "jsonwebtoken": "9.0.2", "jws": "4.0.0",