diff --git a/src/js/ahead-of-time/node/_http_outgoing.ts b/src/js/ahead-of-time/node/_http_outgoing.ts index 040d7409d1e7..ff532fd36be6 100644 --- a/src/js/ahead-of-time/node/_http_outgoing.ts +++ b/src/js/ahead-of-time/node/_http_outgoing.ts @@ -1,5 +1,5 @@ // What the compiler reads in place of src/js/node/_http_outgoing.ts, which `bun run` evaluates. The two do the same. -// Written against that file with the SHA-256 17a5f190f17251c9b4bea8ce90880918a91fffc6329dddc4d5ee859370eb9709. +// Written against that file with the SHA-256 88cc609e812d2ff8933707581fee5dd0d2bb35619f0dbcfe268a07ec05c0e9f4. // When that file changes, bring the change here and write the new hash: test/internal/second-texts-of-modules.test.ts. // Hardcoded module "node:_http_outgoing" @@ -147,7 +147,7 @@ function isContentDispositionField(s: string) { declare global { namespace $Node { - interface OutgoingMessageOptions extends EventEmitterOptions { + interface OutgoingMessageOptions { highWaterMark?: number; rejectNonStandardBodyWrites?: boolean; } @@ -282,12 +282,11 @@ function OutgoingMessage( rejectNonStandardBodyWrites: boolean | undefined = undefined, keepAliveTimeout: number | undefined = undefined, ) { + Stream.$call(this, undefined); if (options === kHandle) { - Stream.$call(this, undefined); highWaterMark ??= getDefaultHighWaterMark(); if (rejectNonStandardBodyWrites === undefined) rejectNonStandardBodyWrites = false; } else { - Stream.$call(this, options); keepAliveTimeout = 0; } diff --git a/src/js/node/_http_outgoing.ts b/src/js/node/_http_outgoing.ts index 6e5276f4afd1..68892116f619 100644 --- a/src/js/node/_http_outgoing.ts +++ b/src/js/node/_http_outgoing.ts @@ -136,7 +136,8 @@ function isContentDispositionField(s) { } function OutgoingMessage(options) { - Stream.$call(this, options); + // As in node, Stream gets no options. `undefined` and not a missing argument: that call needs no arity fix-up. + Stream.$call(this, undefined); // Queue that holds all currently pending data, until the response will be // assigned to the socket (until it will its turn in the HTTP pipeline). diff --git a/src/js/node/child_process.ts b/src/js/node/child_process.ts index 3102c2a468c4..cdd532bfe1d5 100644 --- a/src/js/node/child_process.ts +++ b/src/js/node/child_process.ts @@ -1161,6 +1161,11 @@ class ChildProcess extends EventEmitter { channel; killed = false; + // Not the default constructor, which hands its arguments to EventEmitter. Node's takes none. + constructor() { + super(); + } + [Symbol.dispose]() { if (!this.killed) { this.kill(); diff --git a/test/aot/parallel/library-modules/http-messages-hand-no-options-to-the-emitter.mjs b/test/aot/parallel/library-modules/http-messages-hand-no-options-to-the-emitter.mjs new file mode 100644 index 000000000000..d0080ee410a2 --- /dev/null +++ b/test/aot/parallel/library-modules/http-messages-hand-no-options-to-the-emitter.mjs @@ -0,0 +1,46 @@ +// `captureRejections` in the options of an OutgoingMessage or a ServerResponse does nothing, as in node: EventEmitter is handed no options. +import { EventEmitter } from "node:events"; +import http from "node:http"; + +// An emitter that captures rejections calls then() of the promise that a listener returns. +function captures(emitter) { + let called = false; + const promise = Promise.resolve(); + promise.then = () => { + called = true; + }; + emitter.on("asked", () => promise); + emitter.emit("asked"); + return called; +} +const request = () => ({ method: "GET", httpVersionMajor: 1, httpVersionMinor: 1, headers: {} }); +class Message extends http.OutgoingMessage {} +class Response extends http.ServerResponse {} +function Called(req, options) { + http.ServerResponse.call(this, req, options); +} +Object.setPrototypeOf(Called.prototype, http.ServerResponse.prototype); + +for (const [label, make] of [ + ["new OutgoingMessage", options => new http.OutgoingMessage(options)], + ["a class that extends OutgoingMessage", options => new Message(options)], + ["new ServerResponse", options => new http.ServerResponse(request(), options)], + ["a class that extends ServerResponse", options => new Response(request(), options)], + ["ServerResponse called on an object", options => new Called(request(), options)], +]) { + for (const captureRejections of [true, "yes"]) { + let answer; + try { + answer = captures(make({ captureRejections })); + } catch (error) { + answer = error.code; + } + console.log(`${label}, captureRejections ${JSON.stringify(captureRejections)}: ${answer}`); + } + EventEmitter.captureRejections = true; + try { + console.log(`${label}, while EventEmitter.captureRejections is true: ${captures(make())}`); + } finally { + EventEmitter.captureRejections = false; + } +} diff --git a/test/js/node/child_process/child_process-node.test.js b/test/js/node/child_process/child_process-node.test.js index 4a5b29897903..52df3bccfa47 100644 --- a/test/js/node/child_process/child_process-node.test.js +++ b/test/js/node/child_process/child_process-node.test.js @@ -1,6 +1,7 @@ import { bunEnv, bunExe, isWindows } from "harness"; import { createTest } from "node-harness"; import { ChildProcess, exec, fork, spawn } from "node:child_process"; +import { EventEmitter } from "node:events"; import { tmpdir } from "node:os"; import path from "node:path"; import util from "node:util"; @@ -58,6 +59,52 @@ describe("ChildProcess.constructor", () => { it("should be a function", () => { strictEqual(typeof ChildProcess, "function"); }); + + // https://github.com/nodejs/node/blob/v26.3.0/lib/internal/child_process.js#L255-L256 + it("hands its arguments to no EventEmitter, like Node.js", () => { + // An emitter that captures rejections calls then() of the promise that a listener returns. + const captures = emitter => { + let calls = 0; + const promise = Promise.resolve(); + promise.then = () => void calls++; + emitter.on("asked", () => promise); + emitter.emit("asked"); + return calls > 0; + }; + class Heir extends ChildProcess {} + const answers = {}; + for (const [way, make] of [ + ["new ChildProcess", options => new ChildProcess(options)], + ["a class that extends ChildProcess", options => new Heir(options)], + ]) { + const answer = options => { + try { + return captures(make(options)); + } catch (error) { + return error.code; + } + }; + answers[way] = { + "captureRejections: true": answer({ captureRejections: true }), + 'captureRejections: "yes"': answer({ captureRejections: "yes" }), + }; + EventEmitter.captureRejections = true; + try { + answers[way]["while EventEmitter.captureRejections is true"] = answer(); + } finally { + EventEmitter.captureRejections = false; + } + } + const asInNode = { + "captureRejections: true": false, + 'captureRejections: "yes"': false, + "while EventEmitter.captureRejections is true": true, + }; + expect(answers).toEqual({ + "new ChildProcess": asInNode, + "a class that extends ChildProcess": asInNode, + }); + }); }); describe("ChildProcess.spawn()", () => { diff --git a/test/js/node/http/node-http-stores-of-the-response-constructor.test.ts b/test/js/node/http/node-http-stores-of-the-response-constructor.test.ts index ec858f8a0a3b..993723d66fe9 100644 --- a/test/js/node/http/node-http-stores-of-the-response-constructor.test.ts +++ b/test/js/node/http/node-http-stores-of-the-response-constructor.test.ts @@ -17,3 +17,35 @@ test("the setters of a class that extends ServerResponse get every store of its expect(stdout.split("\n")).toEqual(expected.split("\n")); expect(exitCode).toBe(0); }); + +test("OutgoingMessage and ServerResponse ignore captureRejections in their options, like Node.js", async () => { + await using proc = Bun.spawn({ + cmd: [ + bunExe(), + join(import.meta.dir, "../../../aot/parallel/library-modules/http-messages-hand-no-options-to-the-emitter.mjs"), + ], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + // What node v26.3.0 prints: https://github.com/nodejs/node/blob/v26.3.0/lib/_http_outgoing.js#L107 + expect({ stdout: stdout.split("\n"), stderr, exitCode }).toEqual({ + stdout: [ + ...[ + "new OutgoingMessage", + "a class that extends OutgoingMessage", + "new ServerResponse", + "a class that extends ServerResponse", + "ServerResponse called on an object", + ].flatMap(way => [ + `${way}, captureRejections true: false`, + `${way}, captureRejections "yes": false`, + `${way}, while EventEmitter.captureRejections is true: true`, + ]), + "", + ], + stderr: "", + exitCode: 0, + }); +});