Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
714a57944e fix(node:http): fall back upgrade requests to 'request' event when no 'upgrade' listener
When a WebSocket/upgrade request is sent to an `http.createServer()` server
that has no 'upgrade' event listener, Node.js falls back to emitting a
'request' event. Bun was unconditionally emitting the 'upgrade' event, which
was silently dropped when no listener existed.

Check `server.listenerCount("upgrade")` before deciding to emit the upgrade
event, and fall through to normal request handling when no listeners exist.

Closes #26924

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-12 04:31:16 +00:00
2 changed files with 114 additions and 2 deletions

View File

@@ -582,7 +582,8 @@ Server.prototype[kRealListen] = function (tls, port, host, socketPath, reusePort
socket[kRequest] = http_req;
const is_upgrade = http_req.headers.upgrade;
if (!is_upgrade) {
const hasUpgradeListeners = is_upgrade && server.listenerCount("upgrade") > 0;
if (!hasUpgradeListeners) {
if (canUseInternalAssignSocket) {
// ~10% performance improvement in JavaScriptCore due to avoiding .once("close", ...) and removing a listener
assignSocketInternal(http_res, socket);
@@ -601,7 +602,7 @@ Server.prototype[kRealListen] = function (tls, port, host, socketPath, reusePort
http_res.writeHead(503);
http_res.end();
socket.destroy();
} else if (is_upgrade) {
} else if (hasUpgradeListeners) {
server.emit("upgrade", http_req, socket, kEmptyBuffer);
if (!socket._httpMessage) {
if (canUseInternalAssignSocket) {

View File

@@ -0,0 +1,111 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe } from "harness";
test("node:http server falls back upgrade request to 'request' event when no 'upgrade' listener", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
const http = require('node:http');
const server = http.createServer();
const events = [];
server.on('request', (req, res) => {
events.push('request');
res.end();
});
// No 'upgrade' listener registered
server.listen(0, function() {
const port = this.address().port;
// Send a request with Upgrade header using http module
const req = http.request({
hostname: 'localhost',
port,
path: '/',
method: 'GET',
headers: {
'Connection': 'Upgrade',
'Upgrade': 'websocket',
},
}, (res) => {
events.push('response');
res.resume();
res.on('end', () => {
console.log(JSON.stringify(events));
server.close();
});
});
req.end();
});
`,
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
const events = JSON.parse(stdout.trim());
expect(events).toEqual(["request", "response"]);
expect(exitCode).toBe(0);
});
test("node:http server emits 'upgrade' event when listener is registered", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
const http = require('node:http');
const server = http.createServer();
const events = [];
server.on('request', (req, res) => {
events.push('request');
res.end();
});
server.on('upgrade', (req, socket) => {
events.push('upgrade');
socket.end();
});
server.listen(0, function() {
const port = this.address().port;
const req = http.request({
hostname: 'localhost',
port,
path: '/',
method: 'GET',
headers: {
'Connection': 'Upgrade',
'Upgrade': 'websocket',
},
});
req.on('error', () => {});
req.end();
// Give the server time to process
setTimeout(() => {
console.log(JSON.stringify(events));
server.close();
}, 500);
});
`,
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
const events = JSON.parse(stdout.trim());
expect(events).toEqual(["upgrade"]);
expect(exitCode).toBe(0);
});