Compare commits

...

3 Commits

Author SHA1 Message Date
Claude Bot
fef57b9b79 Fix getConnections to match Node.js behavior - validate callback and use process.nextTick
This improves the getConnections() implementation to match Node.js:

1. **Validate callback argument**: Now throws ERR_INVALID_ARG_TYPE if
   callback is not a function, matching Node.js behavior

2. **Async callback**: Uses process.nextTick to call the callback
   asynchronously, matching Node.js behavior. In Node.js, this is
   necessary because it needs to poll worker processes in cluster mode.

3. **Better comments**: Explained why it's async (cluster/worker polling)
   and why it can't error in Bun (no workers yet)

Applied the same fix to both:
- http.Server.getConnections (src/js/node/_http_server.ts:290-296)
- net.Server.getConnections (src/js/node/net.ts:2170-2176)

Added test for validation behavior to ensure:
- Throws for undefined callback
- Throws for null callback
- Throws for non-function values

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-10 23:34:50 +00:00
Claude Bot
d493ce511f Fix WebSocket upgrade hanging - enable streaming for upgrade sockets
This fixes the WebSocket hanging issue where clients would never
receive the upgrade response from the server. The problem was that
upgrade sockets weren't configured for streaming, so writes to the
socket wouldn't actually send data.

Changes:
- Added socket[kEnableStreaming](true) before emitting "upgrade" event
- This enables handle.ondrain which allows socket writes to work properly
- Previously, socket.write() would return true but bytesWritten stayed 0

The fix matches the pattern used for CONNECT method (line 548) where
streaming is already enabled.

Added comprehensive test in http-websocket-upgrade.test.ts that:
- Creates an HTTP server with upgrade handler
- Sends WebSocket upgrade request
- Verifies client receives "101 Switching Protocols" response
- Test passes with fix, times out without fix

This resolves the code-server WebSocket hanging issue where the
WebSocket would connect but never send/receive messages.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-10 23:31:01 +00:00
Claude Bot
c184b079bb Add getConnections() method to http.Server and track connection count
This fixes an issue where code-server and other Express applications
fail with "app.server.getConnections is not a function" when trying
to query active connection counts.

Changes:
- Added _connections counter to http.Server constructor
- Increment _connections when a new socket connection is established
- Decrement _connections when a socket closes
- Added getConnections() method to http.Server prototype that
  matches Node.js behavior

The implementation follows the same pattern as net.Server's
getConnections() method, calling the callback with (null, count)
where count is 0 if the server is closed.

Fixes issue with running code-server on Bun where the application
would fail when checking active connections.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-10 23:23:51 +00:00
4 changed files with 310 additions and 7 deletions

View File

@@ -2,7 +2,13 @@
const EventEmitter: typeof import("node:events").EventEmitter = require("node:events");
const { Duplex, Stream } = require("node:stream");
const { _checkInvalidHeaderChar: checkInvalidHeaderChar } = require("node:_http_common");
const { validateObject, validateLinkHeaderValue, validateBoolean, validateInteger } = require("internal/validators");
const {
validateObject,
validateLinkHeaderValue,
validateBoolean,
validateInteger,
validateFunction,
} = require("internal/validators");
const { ConnResetException } = require("internal/shared");
const { isPrimary } = require("internal/cluster/isPrimary");
@@ -200,6 +206,7 @@ function Server(options, callback): void {
this.listening = false;
this._unref = false;
this._connections = 0;
this.maxRequestsPerSocket = 0;
this[kInternalSocketData] = undefined;
this[tlsSymbol] = null;
@@ -286,6 +293,15 @@ Server.prototype.unref = function () {
return this;
};
Server.prototype.getConnections = function (callback) {
validateFunction(callback, "callback");
// In Node.js, this is async because it needs to poll worker processes in cluster mode.
// In Bun, we don't use workers (yet), so we just read _connections directly.
// We still use process.nextTick to match Node.js's async behavior.
process.nextTick(callback, null, this[serverSymbol] ? this._connections : 0);
return this;
};
Server.prototype.closeAllConnections = function () {
const server = this[serverSymbol];
if (!server) {
@@ -574,6 +590,7 @@ Server.prototype[kRealListen] = function (tls, port, host, socketPath, reusePort
}
if (isSocketNew && !reachedRequestsLimit) {
server._connections++;
server.emit("connection", socket);
}
@@ -599,6 +616,8 @@ Server.prototype[kRealListen] = function (tls, port, host, socketPath, reusePort
http_res.end();
socket.destroy();
} else if (is_upgrade) {
// Enable streaming for WebSocket/upgrade connections
socket[kEnableStreaming](true);
server.emit("upgrade", http_req, socket, kEmptyBuffer);
if (!socket._httpMessage) {
if (canUseInternalAssignSocket) {
@@ -798,6 +817,7 @@ function onServerClientError(ssl: boolean, socket: unknown, errorCode: number, r
}
err.rawPacket = rawPacket;
const nodeSocket = new NodeHTTPServerSocket(self, socket, ssl);
self._connections++;
self.emit("connection", nodeSocket);
self.emit("clientError", err, nodeSocket);
if (nodeSocket.listenerCount("error") > 0) {
@@ -901,6 +921,11 @@ const NodeHTTPServerSocket = class Socket extends Duplex {
req.destroy();
}
}
// Decrement connection count when socket closes
if (this.server && this.server._connections > 0) {
this.server._connections--;
}
}
#onCloseForDestroy(closeCallback) {
this.#onClose();

View File

@@ -2168,12 +2168,11 @@ Server.prototype.address = function address() {
};
Server.prototype.getConnections = function getConnections(callback) {
if (typeof callback === "function") {
//in Bun case we will never error on getConnections
//node only errors if in the middle of the couting the server got disconnected, what never happens in Bun
//if disconnected will only pass null as well and 0 connected
callback(null, this._handle ? this._connections : 0);
}
validateFunction(callback, "callback");
// In Node.js, this is async because it needs to poll worker processes in cluster mode.
// In Bun, we don't use workers (yet), so we just read _connections directly.
// We still use process.nextTick to match Node.js's async behavior.
process.nextTick(callback, null, this._handle ? this._connections : 0);
return this;
};

View File

@@ -0,0 +1,171 @@
import { expect, test } from "bun:test";
import { createServer } from "node:http";
import { Socket } from "node:net";
test("http.Server.getConnections tracks active connections", async () => {
const server = createServer((req, res) => {
res.writeHead(200, { "Content-Type": "text/plain" });
res.end("Hello World");
});
await new Promise<void>(resolve => {
server.listen(0, () => {
resolve();
});
});
const address = server.address();
if (!address || typeof address === "string") {
throw new Error("Server address is not valid");
}
// Check initial connection count
const initialCount = await new Promise<number>((resolve, reject) => {
server.getConnections((err, count) => {
if (err) reject(err);
else resolve(count);
});
});
expect(initialCount).toBe(0);
// Create a connection
const client1 = new Socket();
await new Promise<void>((resolve, reject) => {
client1.connect(address.port, "127.0.0.1", () => {
resolve();
});
client1.on("error", reject);
});
// Write a simple HTTP request
client1.write("GET / HTTP/1.1\r\nHost: localhost\r\n\r\n");
// Wait a bit for the connection to be established
await new Promise(resolve => setTimeout(resolve, 100));
// Check connection count after first connection
const countAfterFirst = await new Promise<number>((resolve, reject) => {
server.getConnections((err, count) => {
if (err) reject(err);
else resolve(count);
});
});
expect(countAfterFirst).toBe(1);
// Create another connection
const client2 = new Socket();
await new Promise<void>((resolve, reject) => {
client2.connect(address.port, "127.0.0.1", () => {
resolve();
});
client2.on("error", reject);
});
client2.write("GET / HTTP/1.1\r\nHost: localhost\r\n\r\n");
await new Promise(resolve => setTimeout(resolve, 100));
// Check connection count with two connections
const countWithTwo = await new Promise<number>((resolve, reject) => {
server.getConnections((err, count) => {
if (err) reject(err);
else resolve(count);
});
});
expect(countWithTwo).toBe(2);
// Close first connection
client1.end();
await new Promise(resolve => setTimeout(resolve, 100));
// Check connection count after closing one
const countAfterClosingOne = await new Promise<number>((resolve, reject) => {
server.getConnections((err, count) => {
if (err) reject(err);
else resolve(count);
});
});
expect(countAfterClosingOne).toBe(1);
// Close second connection
client2.end();
await new Promise(resolve => setTimeout(resolve, 100));
// Check connection count after closing all
const finalCount = await new Promise<number>((resolve, reject) => {
server.getConnections((err, count) => {
if (err) reject(err);
else resolve(count);
});
});
expect(finalCount).toBe(0);
// Close the server
await new Promise<void>((resolve, reject) => {
server.close(err => {
if (err) reject(err);
else resolve();
});
});
});
test("http.Server.getConnections returns 0 when server is closed", async () => {
const server = createServer((req, res) => {
res.end("OK");
});
await new Promise<void>(resolve => {
server.listen(0, () => {
resolve();
});
});
// Close the server immediately
await new Promise<void>((resolve, reject) => {
server.close(err => {
if (err) reject(err);
else resolve();
});
});
// Check connection count on closed server
const count = await new Promise<number>((resolve, reject) => {
server.getConnections((err, count) => {
if (err) reject(err);
else resolve(count);
});
});
expect(count).toBe(0);
});
test("http.Server.getConnections throws if callback is not a function", async () => {
const server = createServer();
await new Promise<void>(resolve => {
server.listen(0, () => {
resolve();
});
});
// Should throw for undefined
expect(() => {
(server as any).getConnections();
}).toThrow('The "callback" argument must be of type function');
// Should throw for null
expect(() => {
(server as any).getConnections(null);
}).toThrow('The "callback" argument must be of type function');
// Should throw for non-function
expect(() => {
(server as any).getConnections(123);
}).toThrow('The "callback" argument must be of type function');
await new Promise<void>((resolve, reject) => {
server.close(err => {
if (err) reject(err);
else resolve();
});
});
});

View File

@@ -0,0 +1,108 @@
import { expect, test } from "bun:test";
import { createHash } from "node:crypto";
import { createServer } from "node:http";
import { Socket } from "node:net";
test("http.Server WebSocket upgrade sends response correctly", async () => {
const server = createServer((req, res) => {
res.writeHead(200);
res.end("OK");
});
let upgradeReceived = false;
let clientReceivedUpgrade = false;
server.on("upgrade", (request, socket, head) => {
upgradeReceived = true;
const key = request.headers["sec-websocket-key"];
expect(key).toBeTruthy();
if (!key) {
socket.destroy();
return;
}
// Generate WebSocket accept key
const acceptKey = createHash("sha1")
.update(key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11")
.digest("base64");
// Send WebSocket upgrade response
const headers = [
"HTTP/1.1 101 Switching Protocols",
"Upgrade: websocket",
"Connection: Upgrade",
`Sec-WebSocket-Accept: ${acceptKey}`,
"",
"",
].join("\r\n");
socket.write(headers);
});
await new Promise<void>(resolve => {
server.listen(0, () => {
resolve();
});
});
const address = server.address();
if (!address || typeof address === "string") {
throw new Error("Server address is not valid");
}
// Create WebSocket client
const client = new Socket();
await new Promise<void>((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error("Test timeout - client never received upgrade response"));
}, 2000);
client.connect(address.port, "127.0.0.1", () => {
// Send WebSocket handshake
const key = Buffer.from("testkey123456789").toString("base64");
const handshake = [
"GET /test HTTP/1.1",
"Host: localhost",
"Upgrade: websocket",
"Connection: Upgrade",
`Sec-WebSocket-Key: ${key}`,
"Sec-WebSocket-Version: 13",
"",
"",
].join("\r\n");
client.write(handshake);
});
client.on("data", data => {
const response = data.toString();
if (response.includes("101 Switching Protocols")) {
clientReceivedUpgrade = true;
clearTimeout(timeout);
client.end();
resolve();
}
});
client.on("error", err => {
clearTimeout(timeout);
reject(err);
});
});
// Verify both server and client processed the upgrade
expect(upgradeReceived).toBe(true);
expect(clientReceivedUpgrade).toBe(true);
// Clean up
await new Promise<void>((resolve, reject) => {
server.close(err => {
if (err) reject(err);
else resolve();
});
});
});