mirror of
https://github.com/oven-sh/bun
synced 2026-02-04 16:08:53 +00:00
Compare commits
3 Commits
dylan/pyth
...
claude/fix
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
206ad3427b | ||
|
|
38a3fae22f | ||
|
|
548798c5c0 |
@@ -412,6 +412,42 @@ const IncomingMessagePrototype = {
|
||||
set socket(value) {
|
||||
this[fakeSocketSymbol] = value;
|
||||
},
|
||||
// Used by HTTP parser for adding headers from raw socket parsing
|
||||
// This is needed for allowHTTP1 fallback in HTTP/2 servers
|
||||
_addHeaderLine(field, value, dest) {
|
||||
const lowercased = field.toLowerCase();
|
||||
if (lowercased === "set-cookie") {
|
||||
if (dest[lowercased] !== undefined) {
|
||||
dest[lowercased].push(value);
|
||||
} else {
|
||||
dest[lowercased] = [value];
|
||||
}
|
||||
} else {
|
||||
if (dest[lowercased] !== undefined) {
|
||||
dest[lowercased] += ", " + value;
|
||||
} else {
|
||||
dest[lowercased] = value;
|
||||
}
|
||||
}
|
||||
},
|
||||
_addHeaderLines(headers, n) {
|
||||
if (headers?.length) {
|
||||
if (!this.headers) {
|
||||
this.headers = Object.create(null);
|
||||
}
|
||||
if (!this.rawHeaders) {
|
||||
this.rawHeaders = [];
|
||||
}
|
||||
// Add to rawHeaders
|
||||
for (let i = 0; i < n; i++) {
|
||||
this.rawHeaders.push(headers[i]);
|
||||
}
|
||||
// Parse and add to headers object
|
||||
for (let i = 0; i < n; i += 2) {
|
||||
this._addHeaderLine(headers[i], headers[i + 1], this.headers);
|
||||
}
|
||||
}
|
||||
},
|
||||
} satisfies typeof import("node:http").IncomingMessage.prototype;
|
||||
IncomingMessage.prototype = IncomingMessagePrototype;
|
||||
$setPrototypeDirect.$call(IncomingMessage, Readable);
|
||||
|
||||
@@ -29,6 +29,8 @@
|
||||
const { isTypedArray } = require("node:util/types");
|
||||
const { hideFromStack, throwNotImplemented } = require("internal/shared");
|
||||
const { STATUS_CODES } = require("internal/http");
|
||||
const http = require("node:http");
|
||||
const { parsers, freeParser, kIncomingMessage, HTTPParser } = require("node:_http_common");
|
||||
const tls = require("node:tls");
|
||||
const net = require("node:net");
|
||||
const fs = require("node:fs");
|
||||
@@ -59,6 +61,7 @@ const RegExpPrototypeExec = RegExp.prototype.exec;
|
||||
const ObjectAssign = Object.assign;
|
||||
const ArrayIsArray = Array.isArray;
|
||||
const ObjectKeys = Object.keys;
|
||||
const ObjectEntries = Object.entries;
|
||||
const FunctionPrototypeBind = Function.prototype.bind;
|
||||
const StringPrototypeTrim = String.prototype.trim;
|
||||
const ArrayPrototypePush = Array.prototype.push;
|
||||
@@ -3711,13 +3714,278 @@ function closeAllSessions(server: Http2Server | Http2SecureServer) {
|
||||
}
|
||||
}
|
||||
|
||||
// Simple HTTP/1.1 ServerResponse for allowHTTP1 fallback
|
||||
// This writes directly to the socket since we don't have a native Bun handle
|
||||
class Http1FallbackResponse extends EventEmitter {
|
||||
socket;
|
||||
statusCode = 200;
|
||||
statusMessage = "OK";
|
||||
headersSent = false;
|
||||
finished = false;
|
||||
_headers = Object.create(null);
|
||||
req;
|
||||
sendDate = true;
|
||||
_hasBody = true;
|
||||
|
||||
constructor(req, socket) {
|
||||
super();
|
||||
this.req = req;
|
||||
this.socket = socket;
|
||||
if (req.method === "HEAD") this._hasBody = false;
|
||||
}
|
||||
|
||||
setHeader(name, value) {
|
||||
this._headers[StringPrototypeToLowerCase.$call(name)] = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
getHeader(name) {
|
||||
return this._headers[StringPrototypeToLowerCase.$call(name)];
|
||||
}
|
||||
|
||||
removeHeader(name) {
|
||||
delete this._headers[StringPrototypeToLowerCase.$call(name)];
|
||||
return this;
|
||||
}
|
||||
|
||||
hasHeader(name) {
|
||||
return StringPrototypeToLowerCase.$call(name) in this._headers;
|
||||
}
|
||||
|
||||
getHeaderNames() {
|
||||
return ObjectKeys(this._headers);
|
||||
}
|
||||
|
||||
writeHead(statusCode, statusMessage?, headers?) {
|
||||
if (this.headersSent) return this;
|
||||
|
||||
if (typeof statusMessage === "object") {
|
||||
headers = statusMessage;
|
||||
statusMessage = undefined;
|
||||
}
|
||||
|
||||
this.statusCode = statusCode;
|
||||
if (statusMessage) this.statusMessage = statusMessage;
|
||||
else this.statusMessage = STATUS_CODES[statusCode] || "Unknown";
|
||||
|
||||
if (headers) {
|
||||
for (const key of ObjectKeys(headers)) {
|
||||
this.setHeader(key, headers[key]);
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
_flushHeaders() {
|
||||
if (this.headersSent) return;
|
||||
this.headersSent = true;
|
||||
|
||||
let head = `HTTP/1.1 ${this.statusCode} ${this.statusMessage}\r\n`;
|
||||
|
||||
if (this.sendDate && !this.hasHeader("date")) {
|
||||
head += `Date: ${utcDate()}\r\n`;
|
||||
}
|
||||
|
||||
for (const [name, value] of ObjectEntries(this._headers)) {
|
||||
if (ArrayIsArray(value)) {
|
||||
for (const v of value) {
|
||||
head += `${name}: ${v}\r\n`;
|
||||
}
|
||||
} else {
|
||||
head += `${name}: ${value}\r\n`;
|
||||
}
|
||||
}
|
||||
|
||||
head += "\r\n";
|
||||
this.socket.write(head);
|
||||
}
|
||||
|
||||
write(chunk, encoding?, callback?) {
|
||||
if (typeof encoding === "function") {
|
||||
callback = encoding;
|
||||
encoding = undefined;
|
||||
}
|
||||
|
||||
if (!this.headersSent) {
|
||||
if (!this.hasHeader("transfer-encoding") && !this.hasHeader("content-length")) {
|
||||
this.setHeader("transfer-encoding", "chunked");
|
||||
}
|
||||
this._flushHeaders();
|
||||
}
|
||||
|
||||
if (this._hasBody && chunk) {
|
||||
if (this.getHeader("transfer-encoding") === "chunked") {
|
||||
const len = typeof chunk === "string" ? Buffer.byteLength(chunk, encoding) : chunk.length;
|
||||
this.socket.write(len.toString(16) + "\r\n");
|
||||
this.socket.write(chunk, encoding);
|
||||
return this.socket.write("\r\n", undefined, callback);
|
||||
} else {
|
||||
return this.socket.write(chunk, encoding, callback);
|
||||
}
|
||||
} else if (callback) {
|
||||
callback();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
end(data?, encoding?, callback?) {
|
||||
if (typeof data === "function") {
|
||||
callback = data;
|
||||
data = undefined;
|
||||
} else if (typeof encoding === "function") {
|
||||
callback = encoding;
|
||||
encoding = undefined;
|
||||
}
|
||||
|
||||
if (this.finished) {
|
||||
if (callback) process.nextTick(callback);
|
||||
return this;
|
||||
}
|
||||
|
||||
if (!this.headersSent) {
|
||||
if (data && !this.hasHeader("content-length") && !this.hasHeader("transfer-encoding")) {
|
||||
const len = typeof data === "string" ? Buffer.byteLength(data, encoding) : (data?.length ?? 0);
|
||||
this.setHeader("content-length", String(len));
|
||||
} else if (!data && !this.hasHeader("content-length") && !this.hasHeader("transfer-encoding")) {
|
||||
this.setHeader("content-length", "0");
|
||||
}
|
||||
this._flushHeaders();
|
||||
}
|
||||
|
||||
if (this._hasBody && data) {
|
||||
if (this.getHeader("transfer-encoding") === "chunked") {
|
||||
const len = typeof data === "string" ? Buffer.byteLength(data, encoding) : data.length;
|
||||
this.socket.write(len.toString(16) + "\r\n");
|
||||
this.socket.write(data, encoding);
|
||||
this.socket.write("\r\n0\r\n\r\n");
|
||||
} else {
|
||||
this.socket.write(data, encoding);
|
||||
}
|
||||
} else if (this.getHeader("transfer-encoding") === "chunked") {
|
||||
this.socket.write("0\r\n\r\n");
|
||||
}
|
||||
|
||||
this.finished = true;
|
||||
this.emit("finish");
|
||||
|
||||
if (callback) process.nextTick(callback);
|
||||
return this;
|
||||
}
|
||||
|
||||
writeContinue() {
|
||||
this.socket.write("HTTP/1.1 100 Continue\r\n\r\n");
|
||||
}
|
||||
|
||||
destroy(err?) {
|
||||
this.socket.destroy(err);
|
||||
return this;
|
||||
}
|
||||
|
||||
assignSocket(socket) {
|
||||
this.socket = socket;
|
||||
}
|
||||
}
|
||||
|
||||
// HTTP/1.1 connection listener for allowHTTP1 fallback
|
||||
// This sets up an HTTP parser to handle HTTP/1.1 requests on the TLS socket
|
||||
function http1ConnectionListener(socket: Socket, options) {
|
||||
const server = this;
|
||||
|
||||
// Ensure socket has server property set
|
||||
socket.server = server;
|
||||
|
||||
// Get configurable classes from options (set by initializeOptions)
|
||||
// Note: We default to Http1FallbackResponse because http.ServerResponse requires
|
||||
// a native handle that isn't available when parsing raw sockets. Only use a custom
|
||||
// ServerResponse if explicitly provided by the user.
|
||||
const ServerResponse =
|
||||
options.Http1ServerResponse && options.Http1ServerResponse !== http.ServerResponse
|
||||
? options.Http1ServerResponse
|
||||
: Http1FallbackResponse;
|
||||
|
||||
// Get the HTTP parser from the pool
|
||||
const parser = parsers.alloc();
|
||||
parser.socket = socket;
|
||||
socket.parser = parser;
|
||||
|
||||
// Initialize parser for REQUEST mode
|
||||
parser.initialize(HTTPParser.REQUEST, socket);
|
||||
|
||||
// When the parser completes parsing headers, create a response and emit the request event
|
||||
parser.onIncoming = function onIncoming(req, shouldKeepAlive) {
|
||||
// Create a response using the configured ServerResponse class
|
||||
// For Http1FallbackResponse, pass (req, socket). For custom classes, just pass (req).
|
||||
const res = ServerResponse === Http1FallbackResponse ? new ServerResponse(req, socket) : new ServerResponse(req);
|
||||
|
||||
// Emit the request event on the server
|
||||
if (req.headers.expect !== undefined && req.httpVersionMajor === 1 && req.httpVersionMinor === 1) {
|
||||
if (req.headers.expect === "100-continue") {
|
||||
if (server.listenerCount("checkContinue") > 0) {
|
||||
server.emit("checkContinue", req, res);
|
||||
} else {
|
||||
res.writeContinue();
|
||||
server.emit("request", req, res);
|
||||
}
|
||||
} else {
|
||||
if (server.listenerCount("checkExpectation") > 0) {
|
||||
server.emit("checkExpectation", req, res);
|
||||
} else {
|
||||
res.writeHead(417);
|
||||
res.end();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
server.emit("request", req, res);
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
// Set up socket event handlers
|
||||
function onSocketData(data) {
|
||||
const ret = parser.execute(data);
|
||||
if (ret instanceof Error) {
|
||||
socket.destroy(ret);
|
||||
}
|
||||
}
|
||||
|
||||
function onSocketEnd() {
|
||||
const ret = parser.finish();
|
||||
if (ret instanceof Error) {
|
||||
socket.destroy(ret);
|
||||
}
|
||||
}
|
||||
|
||||
function onSocketClose() {
|
||||
// Clean up parser
|
||||
freeParser(parser, null, socket);
|
||||
socket.parser = null;
|
||||
}
|
||||
|
||||
function onSocketError(err) {
|
||||
if (!server.emit("clientError", err, socket)) {
|
||||
socket.destroy(err);
|
||||
}
|
||||
}
|
||||
|
||||
socket.on("data", onSocketData);
|
||||
socket.on("end", onSocketEnd);
|
||||
socket.on("close", onSocketClose);
|
||||
socket.on("error", onSocketError);
|
||||
|
||||
// Emit connection event
|
||||
server.emit("connection", socket);
|
||||
}
|
||||
|
||||
function connectionListener(socket: Socket) {
|
||||
const options = this[bunSocketServerOptions] || {};
|
||||
if (socket.alpnProtocol === false || socket.alpnProtocol === "http/1.1") {
|
||||
// TODO: Fallback to HTTP/1.1
|
||||
// if (options.allowHTTP1 === true) {
|
||||
|
||||
// }
|
||||
// Fallback to HTTP/1.1
|
||||
if (options.allowHTTP1 === true) {
|
||||
return http1ConnectionListener.$call(this, socket, options);
|
||||
}
|
||||
// Let event handler deal with the socket
|
||||
|
||||
if (!this.emit("unknownProtocol", socket)) {
|
||||
@@ -3788,8 +4056,8 @@ function initializeOptions(options) {
|
||||
else options.unknownProtocolTimeout = 10000;
|
||||
|
||||
// Used only with allowHTTP1
|
||||
// options.Http1IncomingMessage ||= http.IncomingMessage;
|
||||
// options.Http1ServerResponse ||= http.ServerResponse;
|
||||
options.Http1IncomingMessage ||= http.IncomingMessage;
|
||||
options.Http1ServerResponse ||= http.ServerResponse;
|
||||
|
||||
options.Http2ServerRequest ||= Http2ServerRequest;
|
||||
options.Http2ServerResponse ||= Http2ServerResponse;
|
||||
@@ -3888,10 +4156,10 @@ class Http2SecureServer extends tls.Server {
|
||||
timeout = 0;
|
||||
[kSessions] = new SafeSet();
|
||||
constructor(options, onRequestHandler) {
|
||||
//TODO: add 'http/1.1' on ALPNProtocols list after allowHTTP1 support
|
||||
if (typeof options !== "undefined") {
|
||||
if (options && typeof options === "object") {
|
||||
options = { ...options, ALPNProtocols: ["h2"] };
|
||||
const ALPNProtocols = options.allowHTTP1 === true ? ["h2", "http/1.1"] : ["h2"];
|
||||
options = { ...options, ALPNProtocols };
|
||||
} else {
|
||||
throw $ERR_INVALID_ARG_TYPE("options", "object", options);
|
||||
}
|
||||
@@ -3899,16 +4167,9 @@ class Http2SecureServer extends tls.Server {
|
||||
options = { ALPNProtocols: ["h2"] };
|
||||
}
|
||||
|
||||
const settings = options.settings;
|
||||
if (typeof settings !== "undefined") {
|
||||
validateObject(settings, "options.settings");
|
||||
}
|
||||
if (options.maxSessionInvalidFrames !== undefined)
|
||||
validateUint32(options.maxSessionInvalidFrames, "maxSessionInvalidFrames");
|
||||
// Initialize options with defaults (including Http1IncomingMessage/ServerResponse)
|
||||
options = initializeOptions(options);
|
||||
|
||||
if (options.maxSessionRejectedStreams !== undefined) {
|
||||
validateUint32(options.maxSessionRejectedStreams, "maxSessionRejectedStreams");
|
||||
}
|
||||
super(options, connectionListener);
|
||||
this[kSessions] = new SafeSet();
|
||||
this.setMaxListeners(0);
|
||||
|
||||
266
test/regression/issue/26721.test.ts
Normal file
266
test/regression/issue/26721.test.ts
Normal file
@@ -0,0 +1,266 @@
|
||||
/**
|
||||
* Regression test for issue #26721
|
||||
*
|
||||
* HTTP/1.1 fallback is broken for `node:http2` secure server when
|
||||
* `allowHTTP1: true` is passed. The server only advertises `h2` in ALPN
|
||||
* negotiation, causing HTTP/1.1-only clients to fail.
|
||||
*
|
||||
* @see https://github.com/oven-sh/bun/issues/26721
|
||||
*/
|
||||
|
||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||
import { readFileSync } from "node:fs";
|
||||
import http2 from "node:http2";
|
||||
import https from "node:https";
|
||||
import { join } from "node:path";
|
||||
|
||||
// TLS certificates for testing
|
||||
const fixturesDir = join(import.meta.dirname, "..", "fixtures");
|
||||
const tlsOptions = {
|
||||
cert: readFileSync(join(fixturesDir, "cert.pem")),
|
||||
key: readFileSync(join(fixturesDir, "cert.key")),
|
||||
};
|
||||
|
||||
interface TestContext {
|
||||
server: http2.Http2SecureServer;
|
||||
serverPort: number;
|
||||
serverUrl: string;
|
||||
}
|
||||
|
||||
describe("HTTP/2 allowHTTP1 option", () => {
|
||||
let ctx: TestContext;
|
||||
|
||||
beforeAll(async () => {
|
||||
const server = http2.createSecureServer({
|
||||
...tlsOptions,
|
||||
allowHTTP1: true,
|
||||
});
|
||||
|
||||
// Handle HTTP/2 streams
|
||||
server.on("stream", (stream, headers) => {
|
||||
stream.respond({
|
||||
":status": 200,
|
||||
"content-type": "text/plain",
|
||||
"x-protocol": "h2",
|
||||
});
|
||||
stream.end("ok h2\n");
|
||||
});
|
||||
|
||||
// Handle HTTP/1.1 requests (via allowHTTP1 fallback)
|
||||
// Note: HTTP/2 compatibility also emits 'request' events, but those requests
|
||||
// will have already been handled by the 'stream' handler. We check if headers
|
||||
// have been sent to avoid double-responding.
|
||||
server.on("request", (req, res) => {
|
||||
// Skip if this is from HTTP/2 compat (headers already sent by stream handler)
|
||||
if (res.headersSent) return;
|
||||
res.writeHead(200, {
|
||||
"content-type": "text/plain",
|
||||
"x-protocol": "http1",
|
||||
});
|
||||
res.end("ok http1\n");
|
||||
});
|
||||
|
||||
const { promise: listenPromise, resolve: listenResolve, reject: listenReject } = Promise.withResolvers<number>();
|
||||
server.listen(0, "127.0.0.1", () => {
|
||||
const address = server.address();
|
||||
if (!address || typeof address === "string") {
|
||||
listenReject(new Error("Failed to get server address"));
|
||||
return;
|
||||
}
|
||||
listenResolve(address.port);
|
||||
});
|
||||
server.once("error", listenReject);
|
||||
const serverPort = await listenPromise;
|
||||
|
||||
ctx = {
|
||||
server,
|
||||
serverPort,
|
||||
serverUrl: `https://127.0.0.1:${serverPort}`,
|
||||
};
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
if (ctx?.server) {
|
||||
// Close all active connections first to ensure server.close() completes
|
||||
if (typeof ctx.server.closeAllConnections === "function") {
|
||||
ctx.server.closeAllConnections();
|
||||
}
|
||||
const { promise, resolve } = Promise.withResolvers<void>();
|
||||
ctx.server.close(() => resolve());
|
||||
await promise;
|
||||
}
|
||||
});
|
||||
|
||||
test("HTTP/2 client can connect and make request", async () => {
|
||||
const client = http2.connect(ctx.serverUrl, { rejectUnauthorized: false });
|
||||
|
||||
const response = await new Promise<{ status: number; body: string; protocol: string }>((resolve, reject) => {
|
||||
const req = client.request({ ":path": "/" });
|
||||
|
||||
let body = "";
|
||||
let protocol = "";
|
||||
|
||||
req.on("response", headers => {
|
||||
protocol = headers["x-protocol"] as string;
|
||||
});
|
||||
|
||||
req.on("data", chunk => {
|
||||
body += chunk;
|
||||
});
|
||||
|
||||
req.on("end", () => {
|
||||
resolve({ status: 200, body, protocol });
|
||||
});
|
||||
|
||||
req.on("error", reject);
|
||||
req.end();
|
||||
});
|
||||
|
||||
expect(response.body).toBe("ok h2\n");
|
||||
expect(response.protocol).toBe("h2");
|
||||
|
||||
const { promise: closePromise, resolve: closeResolve } = Promise.withResolvers<void>();
|
||||
client.close(closeResolve);
|
||||
await closePromise;
|
||||
});
|
||||
|
||||
test("HTTP/1.1 client can connect when allowHTTP1 is true (issue #26721)", async () => {
|
||||
// This test verifies that HTTP/1.1 clients can connect to an HTTP/2 server
|
||||
// with allowHTTP1: true. Before the fix, this would fail with:
|
||||
// "tlsv1 alert no application protocol" because the server only
|
||||
// advertised "h2" in ALPN, not "http/1.1".
|
||||
|
||||
const response = await new Promise<{ statusCode: number; body: string; protocol: string }>((resolve, reject) => {
|
||||
const req = https.request(
|
||||
{
|
||||
hostname: "127.0.0.1",
|
||||
port: ctx.serverPort,
|
||||
path: "/",
|
||||
method: "GET",
|
||||
rejectUnauthorized: false,
|
||||
headers: {
|
||||
Connection: "close", // Ensure connection is closed after request
|
||||
},
|
||||
// Force HTTP/1.1 by not specifying ALPNProtocols or by using https module
|
||||
},
|
||||
res => {
|
||||
let body = "";
|
||||
res.on("data", chunk => {
|
||||
body += chunk;
|
||||
});
|
||||
res.on("end", () => {
|
||||
resolve({
|
||||
statusCode: res.statusCode!,
|
||||
body,
|
||||
protocol: res.headers["x-protocol"] as string,
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
req.on("error", reject);
|
||||
req.end();
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.body).toBe("ok http1\n");
|
||||
expect(response.protocol).toBe("http1");
|
||||
});
|
||||
|
||||
test("HTTP/1.1 POST request works with allowHTTP1", async () => {
|
||||
const postData = JSON.stringify({ message: "hello" });
|
||||
|
||||
// Use the shared server from ctx
|
||||
const response = await new Promise<{ statusCode: number; body: string }>((resolve, reject) => {
|
||||
const req = https.request(
|
||||
{
|
||||
hostname: "127.0.0.1",
|
||||
port: ctx.serverPort,
|
||||
path: "/post",
|
||||
method: "POST",
|
||||
rejectUnauthorized: false,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Content-Length": Buffer.byteLength(postData),
|
||||
Connection: "close", // Ensure connection is closed after request
|
||||
},
|
||||
},
|
||||
res => {
|
||||
let body = "";
|
||||
res.on("data", chunk => {
|
||||
body += chunk;
|
||||
});
|
||||
res.on("end", () => {
|
||||
resolve({ statusCode: res.statusCode!, body });
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
req.on("error", reject);
|
||||
req.write(postData);
|
||||
req.end();
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.body).toBe("ok http1\n");
|
||||
});
|
||||
});
|
||||
|
||||
describe("HTTP/2 without allowHTTP1", () => {
|
||||
test("HTTP/1.1 client gets rejected when allowHTTP1 is false", async () => {
|
||||
const server = http2.createSecureServer({
|
||||
...tlsOptions,
|
||||
allowHTTP1: false,
|
||||
});
|
||||
|
||||
server.on("stream", (stream, _headers) => {
|
||||
stream.respond({ ":status": 200 });
|
||||
stream.end("ok");
|
||||
});
|
||||
|
||||
const { promise: listenPromise, resolve: listenResolve, reject: listenReject } = Promise.withResolvers<number>();
|
||||
server.listen(0, "127.0.0.1", () => {
|
||||
const address = server.address();
|
||||
if (!address || typeof address === "string") {
|
||||
listenReject(new Error("Failed to get server address"));
|
||||
return;
|
||||
}
|
||||
listenResolve(address.port);
|
||||
});
|
||||
server.once("error", listenReject);
|
||||
const port = await listenPromise;
|
||||
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const req = https.request(
|
||||
{
|
||||
hostname: "127.0.0.1",
|
||||
port,
|
||||
path: "/",
|
||||
method: "GET",
|
||||
rejectUnauthorized: false,
|
||||
},
|
||||
() => {
|
||||
reject(new Error("Expected connection to fail"));
|
||||
},
|
||||
);
|
||||
|
||||
req.on("error", err => {
|
||||
// We expect an ALPN negotiation error or similar
|
||||
// Note: Bun's https client may report different error messages
|
||||
expect(err.message).toMatch(/no application protocol|ECONNRESET|ECONNREFUSED|socket hang up/i);
|
||||
resolve();
|
||||
});
|
||||
|
||||
req.end();
|
||||
});
|
||||
} finally {
|
||||
// Force close all connections and the server
|
||||
// Use a short timeout to ensure this doesn't hang the test
|
||||
await Promise.race([
|
||||
new Promise<void>(resolve => server.close(() => resolve())),
|
||||
new Promise<void>(resolve => setTimeout(resolve, 500)),
|
||||
]);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user