diff --git a/misctools/lldb/init.lldb b/misctools/lldb/init.lldb index 777696671f..c2749b47fe 100644 --- a/misctools/lldb/init.lldb +++ b/misctools/lldb/init.lldb @@ -8,6 +8,8 @@ # Thread::initializePlatformThreading() in ThreadingPOSIX.cpp) to the JS thread to suspend or resume # it. So stopping the process would just create noise when debugging any long-running script. process handle -p true -s false -n false SIGPWR +process handle -p true -s false -n false SIGUSR1 +process handle -p true -s false -n false SIGUSR2 command script import -c lldb_pretty_printers.py type category enable zig.lang diff --git a/scripts/runner.node.mjs b/scripts/runner.node.mjs index dac4df8028..fd9aee180b 100755 --- a/scripts/runner.node.mjs +++ b/scripts/runner.node.mjs @@ -579,8 +579,11 @@ async function runTests() { const title = relative(cwd, absoluteTestPath).replaceAll(sep, "/"); if (isNodeTest(testPath)) { const testContent = readFileSync(absoluteTestPath, "utf-8"); - const runWithBunTest = - title.includes("needs-test") || testContent.includes("bun:test") || testContent.includes("node:test"); + let runWithBunTest = title.includes("needs-test") || testContent.includes("node:test"); + // don't wanna have a filter for includes("bun:test") but these need our mocks + runWithBunTest ||= title === "test/js/node/test/parallel/test-fs-append-file-flush.js"; + runWithBunTest ||= title === "test/js/node/test/parallel/test-fs-write-file-flush.js"; + runWithBunTest ||= title === "test/js/node/test/parallel/test-fs-write-stream-flush.js"; const subcommand = runWithBunTest ? "test" : "run"; const env = { FORCE_COLOR: "0", diff --git a/test/js/node/http/node-http-response-write-encode-fixture.js b/test/js/bun/test/parallel/test-http-10177-response.write-with-non-ascii-latin1-should-not-cause-duplicated-character-or-segfault.ts similarity index 100% rename from test/js/node/http/node-http-response-write-encode-fixture.js rename to test/js/bun/test/parallel/test-http-10177-response.write-with-non-ascii-latin1-should-not-cause-duplicated-character-or-segfault.ts diff --git a/test/js/bun/test/parallel/test-http-11425-no-payload-limit.ts b/test/js/bun/test/parallel/test-http-11425-no-payload-limit.ts new file mode 100644 index 0000000000..bbff26a213 --- /dev/null +++ b/test/js/bun/test/parallel/test-http-11425-no-payload-limit.ts @@ -0,0 +1,15 @@ +import { createTest } from "node-harness"; +import { once } from "node:events"; +import { Server } from "node:http"; +const { expect } = createTest(import.meta.path); + +await using server = Server((req, res) => { + res.end(); +}); +server.listen(0); +await once(server, "listening"); +const res = await fetch(`http://localhost:${server.address().port}`, { + method: "POST", + body: new Uint8Array(1024 * 1024 * 200), +}); +expect(res.status).toBe(200); diff --git a/test/js/bun/test/parallel/test-http-13373-should-emit-close-and-complete-should-be-true-only-after-close.ts b/test/js/bun/test/parallel/test-http-13373-should-emit-close-and-complete-should-be-true-only-after-close.ts new file mode 100644 index 0000000000..75a24b5a6d --- /dev/null +++ b/test/js/bun/test/parallel/test-http-13373-should-emit-close-and-complete-should-be-true-only-after-close.ts @@ -0,0 +1,19 @@ +import { createTest } from "node-harness"; +import { once } from "node:events"; +import http from "node:http"; +const { expect } = createTest(import.meta.path); + +await using server = http.createServer().listen(0); +await once(server, "listening"); +fetch(`http://localhost:${server.address().port}`) + .then(res => res.text()) + .catch(() => {}); + +const [req, res] = await once(server, "request"); +expect(req.complete).toBe(false); +console.log("ok 1"); +const closeEvent = once(req, "close"); +res.end("hi"); + +await closeEvent; +expect(req.complete).toBe(true); diff --git a/test/js/bun/test/parallel/test-http-3458-must-set-headersSent-to-true-after-headers-are-sent.ts b/test/js/bun/test/parallel/test-http-3458-must-set-headersSent-to-true-after-headers-are-sent.ts new file mode 100644 index 0000000000..f60cbfd45a --- /dev/null +++ b/test/js/bun/test/parallel/test-http-3458-must-set-headersSent-to-true-after-headers-are-sent.ts @@ -0,0 +1,14 @@ +import { once } from "node:events"; +import { createServer } from "node:http"; +import { createTest } from "node-harness"; +const { expect } = createTest(import.meta.path); + +await using server = createServer().listen(0); +await once(server, "listening"); +fetch(`http://localhost:${server.address()!.port}`).then(res => res.text()); +const [req, res] = await once(server, "request"); +expect(res.headersSent).toBe(false); +const { promise, resolve } = Promise.withResolvers(); +res.end("OK", resolve); +await promise; +expect(res.headersSent).toBe(true); diff --git a/test/js/bun/test/parallel/test-http-4415-IncomingMessage-es5.ts b/test/js/bun/test/parallel/test-http-4415-IncomingMessage-es5.ts new file mode 100644 index 0000000000..288b78f8a5 --- /dev/null +++ b/test/js/bun/test/parallel/test-http-4415-IncomingMessage-es5.ts @@ -0,0 +1,32 @@ +import { createTest } from "node-harness"; +import { once } from "node:events"; +import { IncomingMessage, Server } from "node:http"; +const { expect } = createTest(import.meta.path); + +// This matches Node.js: +const im = Object.create(IncomingMessage.prototype); +IncomingMessage.call(im, { url: "/foo" }); +expect(im.url).toBe(""); + +let didCall = false; +function Subclass(...args) { + IncomingMessage.apply(this, args); + didCall = true; +} +Object.setPrototypeOf(Subclass.prototype, IncomingMessage.prototype); +Object.setPrototypeOf(Subclass, IncomingMessage); + +await using server = new Server({ IncomingMessage: Subclass }, (req, res) => { + if (req instanceof Subclass && didCall) { + expect(req.url).toBe("/foo"); + res.writeHead(200, { "Content-Type": "text/plain" }); + res.end("hello"); + } else { + res.writeHead(500, { "Content-Type": "text/plain" }); + res.end("bye"); + } +}); +server.listen(0); +await once(server, "listening"); +const response = await fetch(`http://localhost:${server.address().port}/foo`, { method: "GET" }); +expect(response.status).toBe(200); diff --git a/test/js/bun/test/parallel/test-http-4415-Server-es5.ts b/test/js/bun/test/parallel/test-http-4415-Server-es5.ts new file mode 100644 index 0000000000..557609c76d --- /dev/null +++ b/test/js/bun/test/parallel/test-http-4415-Server-es5.ts @@ -0,0 +1,12 @@ +import { createTest } from "node-harness"; +import { once } from "node:events"; +import { Server } from "node:http"; +const { expect } = createTest(import.meta.path); + +await using server = Server((req, res) => { + res.end(); +}); +server.listen(0); +await once(server, "listening"); +const res = await fetch(`http://localhost:${server.address().port}`); +expect(res.status).toBe(200); diff --git a/test/js/bun/test/parallel/test-http-4415-ServerResponse-es5.ts b/test/js/bun/test/parallel/test-http-4415-ServerResponse-es5.ts new file mode 100644 index 0000000000..ea41295418 --- /dev/null +++ b/test/js/bun/test/parallel/test-http-4415-ServerResponse-es5.ts @@ -0,0 +1,11 @@ +import { createTest } from "node-harness"; +import { ServerResponse } from "node:http"; +const { expect } = createTest(import.meta.path); + +function Response(req) { + ServerResponse.call(this, req); +} +Response.prototype = Object.create(ServerResponse.prototype); +const req = {}; +const res = new Response(req); +expect(res.req).toBe(req); diff --git a/test/js/bun/test/parallel/test-http-4415-ServerResponse-es6.ts b/test/js/bun/test/parallel/test-http-4415-ServerResponse-es6.ts new file mode 100644 index 0000000000..d5737d6274 --- /dev/null +++ b/test/js/bun/test/parallel/test-http-4415-ServerResponse-es6.ts @@ -0,0 +1,12 @@ +import { createTest } from "node-harness"; +import { ServerResponse } from "node:http"; +const { expect } = createTest(import.meta.path); + +class Response extends ServerResponse { + constructor(req) { + super(req); + } +} +const req = {}; +const res = new Response(req); +expect(res.req).toBe(req); diff --git a/test/js/bun/test/parallel/test-http-7480-should-emit-continue-event.ts b/test/js/bun/test/parallel/test-http-7480-should-emit-continue-event.ts new file mode 100644 index 0000000000..30089fff8b --- /dev/null +++ b/test/js/bun/test/parallel/test-http-7480-should-emit-continue-event.ts @@ -0,0 +1,27 @@ +import { createTest } from "node-harness"; +import https from "node:https"; +const { expect } = createTest(import.meta.path); + +// TODO: today we use a workaround to continue event, we need to fix it in the future. + +let receivedContinue = false; +const req = https.request( + "https://example.com", + { headers: { "accept-encoding": "identity", "expect": "100-continue" } }, + res => { + let data = ""; + res.setEncoding("utf8"); + res.on("data", chunk => { + data += chunk; + }); + res.on("end", () => { + expect(receivedContinue).toBe(true); + expect(data).toContain("This domain is for use in illustrative examples in documents"); + process.exit(); + }); + }, +); +req.on("continue", () => { + receivedContinue = true; +}); +req.end(); diff --git a/test/js/bun/test/parallel/test-http-7480-should-not-emit-continue-event.ts b/test/js/bun/test/parallel/test-http-7480-should-not-emit-continue-event.ts new file mode 100644 index 0000000000..f3c72657d6 --- /dev/null +++ b/test/js/bun/test/parallel/test-http-7480-should-not-emit-continue-event.ts @@ -0,0 +1,21 @@ +import { createTest } from "node-harness"; +import https from "node:https"; +const { expect } = createTest(import.meta.path); + +let receivedContinue = false; +const req = https.request("https://example.com", { headers: { "accept-encoding": "identity" } }, res => { + let data = ""; + res.setEncoding("utf8"); + res.on("data", chunk => { + data += chunk; + }); + res.on("end", () => { + expect(receivedContinue).toBe(false); + expect(data).toContain("This domain is for use in illustrative examples in documents"); + process.exit(); + }); +}); +req.on("continue", () => { + receivedContinue = true; +}); +req.end(); diff --git a/test/js/bun/test/parallel/test-http-9242-IncomingMessage-has-constructor.ts b/test/js/bun/test/parallel/test-http-9242-IncomingMessage-has-constructor.ts new file mode 100644 index 0000000000..859796fff2 --- /dev/null +++ b/test/js/bun/test/parallel/test-http-9242-IncomingMessage-has-constructor.ts @@ -0,0 +1,6 @@ +import { createTest } from "node-harness"; +import { IncomingMessage } from "node:http"; +const { expect } = createTest(import.meta.path); + +const im = new IncomingMessage("http://localhost"); +expect(im.constructor).toBe(IncomingMessage); diff --git a/test/js/bun/test/parallel/test-http-9242-OutgoingMessage-has-constructor.ts b/test/js/bun/test/parallel/test-http-9242-OutgoingMessage-has-constructor.ts new file mode 100644 index 0000000000..7454d74ee2 --- /dev/null +++ b/test/js/bun/test/parallel/test-http-9242-OutgoingMessage-has-constructor.ts @@ -0,0 +1,6 @@ +import { createTest } from "node-harness"; +import { OutgoingMessage } from "node:http"; +const { expect } = createTest(import.meta.path); + +const om = new OutgoingMessage(); +expect(om.constructor).toBe(OutgoingMessage); diff --git a/test/js/bun/test/parallel/test-http-9242-Server-has-constructor.ts b/test/js/bun/test/parallel/test-http-9242-Server-has-constructor.ts new file mode 100644 index 0000000000..d273f0580e --- /dev/null +++ b/test/js/bun/test/parallel/test-http-9242-Server-has-constructor.ts @@ -0,0 +1,6 @@ +import { createTest } from "node-harness"; +import { Server } from "node:http"; +const { expect } = createTest(import.meta.path); + +const s = new Server(); +expect(s.constructor).toBe(Server); diff --git a/test/js/bun/test/parallel/test-http-9242-ServerResponse-has-constructor.ts b/test/js/bun/test/parallel/test-http-9242-ServerResponse-has-constructor.ts new file mode 100644 index 0000000000..cf9c628387 --- /dev/null +++ b/test/js/bun/test/parallel/test-http-9242-ServerResponse-has-constructor.ts @@ -0,0 +1,6 @@ +import { createTest } from "node-harness"; +import { ServerResponse } from "node:http"; +const { expect } = createTest(import.meta.path); + +const sr = new ServerResponse({}); +expect(sr.constructor).toBe(ServerResponse); diff --git a/test/js/bun/test/parallel/test-http-Agent-is-configured-correctly.ts b/test/js/bun/test/parallel/test-http-Agent-is-configured-correctly.ts new file mode 100644 index 0000000000..a72fdde5fa --- /dev/null +++ b/test/js/bun/test/parallel/test-http-Agent-is-configured-correctly.ts @@ -0,0 +1,7 @@ +import { createTest } from "node-harness"; +import http from "node:http"; +const { expect } = createTest(import.meta.path); + +const agent = new http.Agent(); +expect(agent.defaultPort).toBe(80); +expect(agent.protocol).toBe("http:"); diff --git a/test/js/bun/test/parallel/test-http-ServerResponse-ClientRequest-field-exposes-agent-getter.ts b/test/js/bun/test/parallel/test-http-ServerResponse-ClientRequest-field-exposes-agent-getter.ts new file mode 100644 index 0000000000..6617841dab --- /dev/null +++ b/test/js/bun/test/parallel/test-http-ServerResponse-ClientRequest-field-exposes-agent-getter.ts @@ -0,0 +1,23 @@ +import { createTest } from "node-harness"; +import { once } from "node:events"; +import http, { createServer } from "node:http"; +const { expect } = createTest(import.meta.path); + +await using server = createServer((req, res) => { + expect(req.url).toBe("/hello"); + res.writeHead(200); + res.end("world"); +}); +server.listen(0); +await once(server, "listening"); +const url = new URL(`http://127.0.0.1:${server.address().port}`); +const { resolve, reject, promise } = Promise.withResolvers(); +http.get(new URL("/hello", url), res => { + try { + expect(res.req.agent.protocol).toBe("http:"); + resolve(); + } catch (e) { + reject(e); + } +}); +await promise; diff --git a/test/js/bun/test/parallel/test-http-asyncDispose-should-work-in-Server.ts b/test/js/bun/test/parallel/test-http-asyncDispose-should-work-in-Server.ts new file mode 100644 index 0000000000..f9d7a7d89a --- /dev/null +++ b/test/js/bun/test/parallel/test-http-asyncDispose-should-work-in-Server.ts @@ -0,0 +1,10 @@ +import { createTest } from "node-harness"; +import { once } from "node:events"; +import http from "node:http"; +const { expect } = createTest(import.meta.path); + +const server = http.createServer(); +await once(server.listen(0), "listening"); +expect(server.listening).toBe(true); +await server[Symbol.asyncDispose](); +expect(server.listening).toBe(false); diff --git a/test/js/bun/test/parallel/test-http-can-send-brotli-from-Server-and-receive-with-Client.ts b/test/js/bun/test/parallel/test-http-can-send-brotli-from-Server-and-receive-with-Client.ts new file mode 100644 index 0000000000..c4a6a46a71 --- /dev/null +++ b/test/js/bun/test/parallel/test-http-can-send-brotli-from-Server-and-receive-with-Client.ts @@ -0,0 +1,45 @@ +import { createTest } from "node-harness"; +import { once } from "node:events"; +import http, { createServer } from "node:http"; +import * as stream from "node:stream"; +import * as zlib from "node:zlib"; +const { expect } = createTest(import.meta.path); + +await using server = createServer((req, res) => { + expect(req.url).toBe("/hello"); + res.writeHead(200); + res.setHeader("content-encoding", "br"); + + const inputStream = new stream.Readable(); + inputStream.push("Hello World"); + inputStream.push(null); + + const passthrough = new stream.PassThrough(); + passthrough.on("data", data => res.write(data)); + passthrough.on("end", () => res.end()); + + inputStream.pipe(zlib.createBrotliCompress()).pipe(passthrough); +}); + +server.listen(0); +await once(server, "listening"); +const url = new URL(`http://127.0.0.1:${server.address().port}`); + +const { resolve, reject, promise } = Promise.withResolvers(); +http.get(new URL("/hello", url), res => { + let rawData = ""; + const passthrough = stream.PassThrough(); + passthrough.on("data", chunk => { + rawData += chunk; + }); + passthrough.on("end", () => { + try { + expect(Buffer.from(rawData)).toEqual(Buffer.from("Hello World")); + resolve(); + } catch (e) { + reject(e); + } + }); + res.pipe(zlib.createBrotliDecompress()).pipe(passthrough); +}); +await promise; diff --git a/test/js/bun/test/parallel/test-http-can-send-brotli-from-Server-and-receive-with-fetch.ts b/test/js/bun/test/parallel/test-http-can-send-brotli-from-Server-and-receive-with-fetch.ts new file mode 100644 index 0000000000..4aae3f9919 --- /dev/null +++ b/test/js/bun/test/parallel/test-http-can-send-brotli-from-Server-and-receive-with-fetch.ts @@ -0,0 +1,24 @@ +import { createTest } from "node-harness"; +import { once } from "node:events"; +import { createServer } from "node:http"; +import * as stream from "node:stream"; +import * as zlib from "node:zlib"; +const { expect } = createTest(import.meta.path); + +await using server = createServer((req, res) => { + expect(req.url).toBe("/hello"); + res.writeHead(200); + res.setHeader("content-encoding", "br"); + + const inputStream = new stream.Readable(); + inputStream.push("Hello World"); + inputStream.push(null); + + inputStream.pipe(zlib.createBrotliCompress()).pipe(res); +}); +server.listen(0); +await once(server, "listening"); +const url = new URL(`http://127.0.0.1:${server.address().port}`); + +const res = await fetch(new URL("/hello", url)); +expect(await res.text()).toBe("Hello World"); diff --git a/test/js/bun/test/parallel/test-http-can-send-deflate-from-Server-and-receive-with-fetch.ts b/test/js/bun/test/parallel/test-http-can-send-deflate-from-Server-and-receive-with-fetch.ts new file mode 100644 index 0000000000..99851ca210 --- /dev/null +++ b/test/js/bun/test/parallel/test-http-can-send-deflate-from-Server-and-receive-with-fetch.ts @@ -0,0 +1,23 @@ +import { createTest } from "node-harness"; +import { once } from "node:events"; +import { createServer } from "node:http"; +import * as stream from "node:stream"; +import * as zlib from "node:zlib"; +const { expect } = createTest(import.meta.path); + +await using server = createServer((req, res) => { + expect(req.url).toBe("/hello"); + res.writeHead(200); + res.setHeader("content-encoding", "deflate"); + + const inputStream = new stream.Readable(); + inputStream.push("Hello World"); + inputStream.push(null); + + inputStream.pipe(zlib.createDeflate()).pipe(res); +}); +server.listen(0); +await once(server, "listening"); +const url = new URL(`http://127.0.0.1:${server.address().port}`); +const res = await fetch(new URL("/hello", url)); +expect(await res.text()).toBe("Hello World"); diff --git a/test/js/bun/test/parallel/test-http-can-send-gzip-from-Server-and-receive-with-fetch.ts b/test/js/bun/test/parallel/test-http-can-send-gzip-from-Server-and-receive-with-fetch.ts new file mode 100644 index 0000000000..2058a1a01e --- /dev/null +++ b/test/js/bun/test/parallel/test-http-can-send-gzip-from-Server-and-receive-with-fetch.ts @@ -0,0 +1,23 @@ +import { createTest } from "node-harness"; +import { once } from "node:events"; +import { createServer } from "node:http"; +import * as stream from "node:stream"; +import * as zlib from "node:zlib"; +const { expect } = createTest(import.meta.path); + +await using server = createServer((req, res) => { + expect(req.url).toBe("/hello"); + res.writeHead(200); + res.setHeader("content-encoding", "gzip"); + + const inputStream = new stream.Readable(); + inputStream.push("Hello World"); + inputStream.push(null); + + inputStream.pipe(zlib.createGzip()).pipe(res); +}); +server.listen(0); +await once(server, "listening"); +const url = new URL(`http://127.0.0.1:${server.address().port}`); +const res = await fetch(new URL("/hello", url)); +expect(await res.text()).toBe("Hello World"); diff --git a/test/js/bun/test/parallel/test-http-chunked-encoding-must be-valid-after-without-flushHeaders.ts b/test/js/bun/test/parallel/test-http-chunked-encoding-must be-valid-after-without-flushHeaders.ts new file mode 100644 index 0000000000..70be9aeb3a --- /dev/null +++ b/test/js/bun/test/parallel/test-http-chunked-encoding-must be-valid-after-without-flushHeaders.ts @@ -0,0 +1,92 @@ +import { createTest } from "node-harness"; +import { once } from "node:events"; +import http from "node:http"; +import { connect } from "node:net"; +const { expect } = createTest(import.meta.path); + +const { promise, resolve, reject } = Promise.withResolvers(); +await using server = http.createServer(async (req, res) => { + res.writeHead(200, { "Content-Type": "text/plain", "Transfer-Encoding": "chunked" }); + // send some chunks at once + res.write("chunk 1"); + res.write("chunk 2"); + res.write("chunk 3"); + res.write("chunk 4"); + res.write("chunk 5"); + await Bun.sleep(10); + // send some more chunk + res.write("chunk 6"); + res.write("chunk 7"); + await Bun.sleep(10); + // send the last chunk + res.end(); +}); + +server.listen(0); +await once(server, "listening"); + +const socket = connect(server.address().port, () => { + socket.write(`GET / HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nConnection: close\r\n\r\n`); +}); + +const chunks = []; +socket.on("data", data => { + chunks.push(data); +}); + +function parseChunkedData(buffer) { + let offset = 0; + let result = Buffer.alloc(0); + + while (offset < buffer.length) { + // Find the CRLF that terminates the chunk size line + let lineEnd = buffer.indexOf("\r\n", offset); + if (lineEnd === -1) break; + + // Parse the chunk size (in hex) + const chunkSizeHex = buffer.toString("ascii", offset, lineEnd); + const chunkSize = parseInt(chunkSizeHex, 16); + expect(isNaN(chunkSize)).toBe(false); + // If chunk size is 0, we've reached the end + if (chunkSize === 0) { + // Skip the final CRLF after the 0-size chunk + offset = lineEnd + 4; + break; + } + + // Move past the chunk size line's CRLF + offset = lineEnd + 2; + + // Extract the chunk data + const chunkData = buffer.slice(offset, offset + chunkSize); + + // Concatenate this chunk to our result + result = Buffer.concat([result, chunkData]); + + // Move past this chunk's data and its terminating CRLF + offset += chunkSize + 2; + } + + return result; +} + +socket.on("end", () => { + try { + const data = Buffer.concat(chunks); + + const headersEnd = data.indexOf("\r\n\r\n"); + const headers = data.toString("utf-8", 0, headersEnd).split("\r\n"); + expect(headers[0]).toBe("HTTP/1.1 200 OK"); + expect(headers[1]).toBe("Content-Type: text/plain"); + expect(headers[2]).toBe("Transfer-Encoding: chunked"); + expect(headers[3].startsWith("Date:")).toBe(true); + const body = parseChunkedData(data.slice(headersEnd + 4)); + expect(body.toString("utf-8")).toBe("chunk 1chunk 2chunk 3chunk 4chunk 5chunk 6chunk 7"); + resolve(); + } catch (e) { + reject(e); + } finally { + socket.end(); + } +}); +await promise; diff --git a/test/js/bun/test/parallel/test-http-chunked-encoding-must-be-valid-after-flushHeaders.ts b/test/js/bun/test/parallel/test-http-chunked-encoding-must-be-valid-after-flushHeaders.ts new file mode 100644 index 0000000000..d1ca0de81c --- /dev/null +++ b/test/js/bun/test/parallel/test-http-chunked-encoding-must-be-valid-after-flushHeaders.ts @@ -0,0 +1,100 @@ +import { createTest } from "node-harness"; +import { once } from "node:events"; +import http from "node:http"; +import { connect } from "node:net"; +const { expect } = createTest(import.meta.path); + +const { promise, resolve, reject } = Promise.withResolvers(); +await using server = http.createServer(async (req, res) => { + res.writeHead(200, { "Content-Type": "text/plain", "Transfer-Encoding": "chunked" }); + res.flushHeaders(); + // make sure headers are flushed + await Bun.sleep(10); + // send some chunks at once + res.write("chunk 1"); + res.write("chunk 2"); + res.write("chunk 3"); + res.write("chunk 4"); + res.write("chunk 5"); + await Bun.sleep(10); + // send some more chunk + res.write("chunk 6"); + res.write("chunk 7"); + await Bun.sleep(10); + // send the last chunk + res.end(); +}); + +server.listen(0); +await once(server, "listening"); + +const socket = connect(server.address().port, () => { + socket.write(`GET / HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nConnection: close\r\n\r\n`); +}); + +const chunks = []; +let received_headers = false; +socket.on("data", data => { + if (!received_headers) { + received_headers = true; + const headers = data.toString("utf-8").split("\r\n"); + expect(headers[0]).toBe("HTTP/1.1 200 OK"); + expect(headers[1]).toBe("Content-Type: text/plain"); + expect(headers[2]).toBe("Transfer-Encoding: chunked"); + expect(headers[3].startsWith("Date:")).toBe(true); + // empty line for end of headers aka flushHeaders works + expect(headers[headers.length - 1]).toBe(""); + expect(headers[headers.length - 2]).toBe(""); + } else { + chunks.push(data); + } +}); + +function parseChunkedData(buffer) { + let offset = 0; + let result = Buffer.alloc(0); + + while (offset < buffer.length) { + // Find the CRLF that terminates the chunk size line + let lineEnd = buffer.indexOf("\r\n", offset); + if (lineEnd === -1) break; + + // Parse the chunk size (in hex) + const chunkSizeHex = buffer.toString("ascii", offset, lineEnd); + const chunkSize = parseInt(chunkSizeHex, 16); + expect(isNaN(chunkSize)).toBe(false); + // If chunk size is 0, we've reached the end + if (chunkSize === 0) { + // Skip the final CRLF after the 0-size chunk + offset = lineEnd + 4; + break; + } + + // Move past the chunk size line's CRLF + offset = lineEnd + 2; + + // Extract the chunk data + const chunkData = buffer.slice(offset, offset + chunkSize); + + // Concatenate this chunk to our result + result = Buffer.concat([result, chunkData]); + + // Move past this chunk's data and its terminating CRLF + offset += chunkSize + 2; + } + + return result; +} + +socket.on("end", () => { + try { + const body = parseChunkedData(Buffer.concat(chunks)); + expect(body.toString("utf-8")).toBe("chunk 1chunk 2chunk 3chunk 4chunk 5chunk 6chunk 7"); + resolve(); + } catch (e) { + reject(e); + } finally { + socket.end(); + } +}); +await promise; diff --git a/test/js/bun/test/parallel/test-http-chunked-encoding-must-be-valid-using-minimal-code.ts b/test/js/bun/test/parallel/test-http-chunked-encoding-must-be-valid-using-minimal-code.ts new file mode 100644 index 0000000000..0a6369754f --- /dev/null +++ b/test/js/bun/test/parallel/test-http-chunked-encoding-must-be-valid-using-minimal-code.ts @@ -0,0 +1,81 @@ +import { createTest } from "node-harness"; +import { once } from "node:events"; +import http from "node:http"; +import { connect } from "node:net"; +const { expect } = createTest(import.meta.path); + +const { promise, resolve, reject } = Promise.withResolvers(); +await using server = http.createServer(async (req, res) => { + res.writeHead(200, { "Content-Type": "text/plain", "Transfer-Encoding": "chunked" }); + res.write("chunk 1"); + res.end("chunk 2"); +}); + +server.listen(0); +await once(server, "listening"); + +const socket = connect(server.address().port, () => { + socket.write(`GET / HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nConnection: close\r\n\r\n`); +}); + +const chunks = []; +socket.on("data", data => { + chunks.push(data); +}); + +function parseChunkedData(buffer) { + let offset = 0; + let result = Buffer.alloc(0); + + while (offset < buffer.length) { + // Find the CRLF that terminates the chunk size line + let lineEnd = buffer.indexOf("\r\n", offset); + if (lineEnd === -1) break; + + // Parse the chunk size (in hex) + const chunkSizeHex = buffer.toString("ascii", offset, lineEnd); + const chunkSize = parseInt(chunkSizeHex, 16); + expect(isNaN(chunkSize)).toBe(false); + // If chunk size is 0, we've reached the end + if (chunkSize === 0) { + // Skip the final CRLF after the 0-size chunk + offset = lineEnd + 4; + break; + } + + // Move past the chunk size line's CRLF + offset = lineEnd + 2; + + // Extract the chunk data + const chunkData = buffer.slice(offset, offset + chunkSize); + + // Concatenate this chunk to our result + result = Buffer.concat([result, chunkData]); + + // Move past this chunk's data and its terminating CRLF + offset += chunkSize + 2; + } + + return result; +} + +socket.on("end", () => { + try { + const data = Buffer.concat(chunks); + + const headersEnd = data.indexOf("\r\n\r\n"); + const headers = data.toString("utf-8", 0, headersEnd).split("\r\n"); + expect(headers[0]).toBe("HTTP/1.1 200 OK"); + expect(headers[1]).toBe("Content-Type: text/plain"); + expect(headers[2]).toBe("Transfer-Encoding: chunked"); + expect(headers[3].startsWith("Date:")).toBe(true); + const body = parseChunkedData(data.slice(headersEnd + 4)); + expect(body.toString("utf-8")).toBe("chunk 1chunk 2"); + resolve(); + } catch (e) { + reject(e); + } finally { + socket.end(); + } +}); +await promise; diff --git a/test/js/bun/test/parallel/test-http-client-should-be-able-to-send-array-of-[key-value]-as-headers.ts b/test/js/bun/test/parallel/test-http-client-should-be-able-to-send-array-of-[key-value]-as-headers.ts new file mode 100644 index 0000000000..f637001012 --- /dev/null +++ b/test/js/bun/test/parallel/test-http-client-should-be-able-to-send-array-of-[key-value]-as-headers.ts @@ -0,0 +1,29 @@ +import { createTest } from "node-harness"; +import { once } from "node:events"; +import http from "node:http"; +import type { AddressInfo } from "node:net"; +const { expect } = createTest(import.meta.path); + +const { promise, resolve } = Promise.withResolvers(); +await using server = http.createServer((req, res) => { + resolve([req, res]); +}); +await once(server.listen(0), "listening"); +const address = server.address() as AddressInfo; +http.get({ + host: "127.0.0.1", + port: address.port, + headers: [ + ["foo", "bar"], + ["foo", "baz"], + ["host", "127.0.0.1"], + ["host", "127.0.0.2"], + ["host", "127.0.0.3"], + ], +}); + +const [req, res] = await promise; +expect(req.headers.foo).toBe("bar, baz"); +expect(req.headers.host).toBe("127.0.0.1"); + +res.end(); diff --git a/test/js/bun/test/parallel/test-http-client-should-use-chunked-encoding-if-more-than-one-write-is-called.ts b/test/js/bun/test/parallel/test-http-client-should-use-chunked-encoding-if-more-than-one-write-is-called.ts new file mode 100644 index 0000000000..22064282f4 --- /dev/null +++ b/test/js/bun/test/parallel/test-http-client-should-use-chunked-encoding-if-more-than-one-write-is-called.ts @@ -0,0 +1,67 @@ +import { createTest } from "node-harness"; +import http from "node:http"; +const { expect } = createTest(import.meta.path); + +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} +// Bun.serve is used here until #15576 or similar fix is merged +using server = Bun.serve({ + port: 0, + hostname: "127.0.0.1", + fetch(req) { + if (req.headers.get("transfer-encoding") !== "chunked") { + return new Response("should be chunked encoding", { status: 500 }); + } + return new Response(req.body); + }, +}); + +// Options for the HTTP request +const options = { + hostname: "127.0.0.1", // Replace with the target server + port: server.port, + path: "/api/data", + method: "POST", + headers: { + "Content-Type": "application/json", + }, +}; + +const { promise, resolve, reject } = Promise.withResolvers(); + +// Create the request +const req = http.request(options, res => { + if (res.statusCode !== 200) { + reject(new Error("Body should be chunked")); + } + const chunks = []; + // Collect the response data + res.on("data", chunk => { + chunks.push(chunk); + }); + + res.on("end", () => { + resolve(chunks); + }); +}); + +// Handle errors +req.on("error", reject); + +// Write chunks to the request body + +for (let i = 0; i < 4; i++) { + req.write("chunk"); + await sleep(50); + req.write(" "); + await sleep(50); +} +req.write("BUN!"); +// End the request and signal no more data will be sent +req.end(); + +const chunks = await promise; +expect(chunks.length).toBeGreaterThan(1); +expect(chunks[chunks.length - 1]?.toString()).toEndWith("BUN!"); +expect(Buffer.concat(chunks).toString()).toBe("chunk ".repeat(4) + "BUN!"); diff --git a/test/js/bun/test/parallel/test-http-client-should-use-content-length-if-only-one-write-is-called.ts b/test/js/bun/test/parallel/test-http-client-should-use-content-length-if-only-one-write-is-called.ts new file mode 100644 index 0000000000..e3afefea09 --- /dev/null +++ b/test/js/bun/test/parallel/test-http-client-should-use-content-length-if-only-one-write-is-called.ts @@ -0,0 +1,59 @@ +import { createTest } from "node-harness"; +import { once } from "node:events"; +import http from "node:http"; +const { expect } = createTest(import.meta.path); + +await using server = http.createServer((req, res) => { + if (req.headers["transfer-encoding"] === "chunked") { + return res.writeHead(500).end(); + } + res.writeHead(200); + req.on("data", data => { + res.write(data); + }); + req.on("end", () => { + res.end(); + }); +}); + +await once(server.listen(0, "127.0.0.1"), "listening"); + +// Options for the HTTP request +const options = { + hostname: "127.0.0.1", // Replace with the target server + port: server.address().port, + path: "/api/data", + method: "POST", + headers: { + "Content-Type": "application/json", + }, +}; + +const { promise, resolve, reject } = Promise.withResolvers(); + +// Create the request +const req = http.request(options, res => { + if (res.statusCode !== 200) { + reject(new Error("Body should not be chunked")); + } + const chunks = []; + // Collect the response data + res.on("data", chunk => { + chunks.push(chunk); + }); + + res.on("end", () => { + resolve(chunks); + }); +}); +// Handle errors +req.on("error", reject); +// Write chunks to the request body +req.write("Hello World BUN!"); +// End the request and signal no more data will be sent +req.end(); + +const chunks = await promise; +expect(chunks.length).toBe(1); +expect(chunks[0]?.toString()).toBe("Hello World BUN!"); +expect(Buffer.concat(chunks).toString()).toBe("Hello World BUN!"); diff --git a/test/js/bun/test/parallel/test-http-client-side-flushHeaders-should-work.ts b/test/js/bun/test/parallel/test-http-client-side-flushHeaders-should-work.ts new file mode 100644 index 0000000000..486a600c34 --- /dev/null +++ b/test/js/bun/test/parallel/test-http-client-side-flushHeaders-should-work.ts @@ -0,0 +1,24 @@ +import { createTest } from "node-harness"; +import { once } from "node:events"; +import http from "node:http"; +import type { AddressInfo } from "node:net"; +const { expect } = createTest(import.meta.path); + +const { promise, resolve } = Promise.withResolvers(); +await using server = http.createServer((req, res) => { + resolve(req.headers); + res.end(); +}); + +await once(server.listen(0), "listening"); +const address = server.address() as AddressInfo; +const req = http.request({ + method: "GET", + host: "127.0.0.1", + port: address.port, +}); +req.setHeader("foo", "bar"); +req.flushHeaders(); +const headers = await promise; +expect(headers).toBeDefined(); +expect(headers.foo).toEqual("bar"); diff --git a/test/js/bun/test/parallel/test-http-clientError-should-fire-when-receiving-invalid-method.ts b/test/js/bun/test/parallel/test-http-clientError-should-fire-when-receiving-invalid-method.ts new file mode 100644 index 0000000000..416cae6c22 --- /dev/null +++ b/test/js/bun/test/parallel/test-http-clientError-should-fire-when-receiving-invalid-method.ts @@ -0,0 +1,24 @@ +import { createTest } from "node-harness"; +import { once } from "node:events"; +import http from "node:http"; +import type { AddressInfo } from "node:net"; +import { createConnection } from "node:net"; +const { expect } = createTest(import.meta.path); + +await using server = http.createServer((req, res) => { + res.end(); +}); +let socket; +server.on("clientError", err => { + expect(err.code).toBe("HPE_INVALID_METHOD"); + expect(err.rawPacket.toString()).toBe("*"); + + socket.end(); +}); +await once(server.listen(0), "listening"); +const address = server.address() as AddressInfo; +socket = createConnection({ port: address.port }); + +await once(socket, "connect"); +socket.write("*"); +await once(socket, "close"); diff --git a/test/js/bun/test/parallel/test-http-destroy-should-end-download.ts b/test/js/bun/test/parallel/test-http-destroy-should-end-download.ts new file mode 100644 index 0000000000..227f44dba4 --- /dev/null +++ b/test/js/bun/test/parallel/test-http-destroy-should-end-download.ts @@ -0,0 +1,54 @@ +import { createTest } from "node-harness"; +import { request } from "node:http"; +const { expect } = createTest(import.meta.path); + +// just simulate some file that will take forever to download +const payload = Buffer.alloc(128 * 1024, "X"); +for (let i = 0; i < 5; i++) { + let sendedByteLength = 0; + using server = Bun.serve({ + port: 0, + async fetch(req) { + let running = true; + req.signal.onabort = () => (running = false); + return new Response(async function* () { + while (running) { + sendedByteLength += payload.byteLength; + yield payload; + await Bun.sleep(10); + } + }); + }, + }); + + async function run() { + let receivedByteLength = 0; + let { promise, resolve } = Promise.withResolvers(); + const req = request(server.url, res => { + res.on("data", data => { + receivedByteLength += data.length; + if (resolve) { + resolve(); + resolve = null; + } + }); + }); + req.end(); + await promise; + req.destroy(); + await Bun.sleep(10); + const initialByteLength = receivedByteLength; + // we should receive the same amount of data we sent + expect(initialByteLength).toBeLessThanOrEqual(sendedByteLength); + await Bun.sleep(10); + // we should not receive more data after destroy + expect(initialByteLength).toBe(receivedByteLength); + await Bun.sleep(10); + } + + const runCount = 50; + const runs = Array.from({ length: runCount }, run); + await Promise.all(runs); + Bun.gc(true); + await Bun.sleep(10); +} diff --git a/test/js/bun/test/parallel/test-http-do-https-request-fails.ts b/test/js/bun/test/parallel/test-http-do-https-request-fails.ts new file mode 100644 index 0000000000..f1ca709d7d --- /dev/null +++ b/test/js/bun/test/parallel/test-http-do-https-request-fails.ts @@ -0,0 +1,9 @@ +import { createTest } from "node-harness"; +import http from "node:http"; +const { expect } = createTest(import.meta.path); + +expect(() => http.request("https://example.com")).toThrow(TypeError); +expect(() => http.request("https://example.com")).toThrow({ + code: "ERR_INVALID_PROTOCOL", + message: `Protocol "https:" not supported. Expected "http:"`, +}); diff --git a/test/js/bun/test/parallel/test-http-empty-requests-should-be-Transfer-Encoding-chunked.ts b/test/js/bun/test/parallel/test-http-empty-requests-should-be-Transfer-Encoding-chunked.ts new file mode 100644 index 0000000000..5d7a6eb2a6 --- /dev/null +++ b/test/js/bun/test/parallel/test-http-empty-requests-should-be-Transfer-Encoding-chunked.ts @@ -0,0 +1,52 @@ +import { createTest } from "node-harness"; +import { once } from "node:events"; +import http from "node:http"; +const { expect } = createTest(import.meta.path); + +await using server = http.createServer((req, res) => { + res.end(JSON.stringify(req.headers)); +}); +await once(server.listen(0), "listening"); +const url = `http://localhost:${server.address().port}`; +for (let method of ["GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"]) { + const { promise, resolve, reject } = Promise.withResolvers(); + http + .request( + url, + { + method, + }, + res => { + const body: Uint8Array[] = []; + res.on("data", chunk => { + body.push(chunk); + }); + res.on("end", () => { + try { + resolve(JSON.parse(Buffer.concat(body).toString())); + } catch (e) { + reject(e); + } + }); + }, + ) + .on("error", reject) + .end(); + + const headers = (await promise) as Record; + expect(headers).toBeDefined(); + expect(headers["transfer-encoding"]).toBeUndefined(); + switch (method) { + case "GET": + case "DELETE": + case "OPTIONS": + // Content-Length will not be present for GET, DELETE, and OPTIONS + // aka DELETE in node.js will be undefined and in bun it will be 0 + // this is not outside the spec but is different between node.js and bun + expect(headers["content-length"]).toBeOneOf(["0", undefined]); + break; + default: + expect(headers["content-length"]).toBeDefined(); + break; + } +} diff --git a/test/js/bun/test/parallel/test-http-flushHeaders-should-not-drop-request-body.ts b/test/js/bun/test/parallel/test-http-flushHeaders-should-not-drop-request-body.ts new file mode 100644 index 0000000000..edd337ecd6 --- /dev/null +++ b/test/js/bun/test/parallel/test-http-flushHeaders-should-not-drop-request-body.ts @@ -0,0 +1,32 @@ +import { createTest } from "node-harness"; +import { once } from "node:events"; +import http from "node:http"; +import type { AddressInfo } from "node:net"; +const { expect } = createTest(import.meta.path); + +const { promise, resolve } = Promise.withResolvers(); +await using server = http.createServer((req, res) => { + let body = ""; + req.setEncoding("utf8"); + req.on("data", chunk => (body += chunk)); + req.on("end", () => { + resolve(body); + res.end(); + }); +}); + +await once(server.listen(0), "listening"); +const address = server.address() as AddressInfo; +const req = http.request({ + method: "POST", + host: "127.0.0.1", + port: address.port, + headers: { "content-type": "text/plain" }, +}); + +req.flushHeaders(); +req.write("bun"); +req.end("rocks"); + +const body = await promise; +expect(body).toBe("bunrocks"); diff --git a/test/js/bun/test/parallel/test-http-get-can-use-Agent.ts b/test/js/bun/test/parallel/test-http-get-can-use-Agent.ts new file mode 100644 index 0000000000..06452b9380 --- /dev/null +++ b/test/js/bun/test/parallel/test-http-get-can-use-Agent.ts @@ -0,0 +1,10 @@ +import { createTest } from "node-harness"; +import http from "node:http"; +const { expect } = createTest(import.meta.path); + +const agent = new http.Agent(); +const { promise, resolve } = Promise.withResolvers(); +http.get({ agent, hostname: "google.com" }, resolve); +const response = await promise; +expect(response.req.port).toBe(80); +expect(response.req.protocol).toBe("http:"); diff --git a/test/js/bun/test/parallel/test-http-host-array-should-throw-in-request.ts b/test/js/bun/test/parallel/test-http-host-array-should-throw-in-request.ts new file mode 100644 index 0000000000..6e39abb123 --- /dev/null +++ b/test/js/bun/test/parallel/test-http-host-array-should-throw-in-request.ts @@ -0,0 +1,7 @@ +import { createTest } from "node-harness"; +import http from "node:http"; +const { expect } = createTest(import.meta.path); + +expect(() => http.request({ host: [1, 2, 3] })).toThrow( + 'The "options.host" property must be of type string, undefined, or null. Received an instance of Array', +); diff --git a/test/js/bun/test/parallel/test-http-must-set-headersSent-to-true-after-headers-are-sent-when-using-chunk-encoded.ts b/test/js/bun/test/parallel/test-http-must-set-headersSent-to-true-after-headers-are-sent-when-using-chunk-encoded.ts new file mode 100644 index 0000000000..6a058a3da1 --- /dev/null +++ b/test/js/bun/test/parallel/test-http-must-set-headersSent-to-true-after-headers-are-sent-when-using-chunk-encoded.ts @@ -0,0 +1,18 @@ +import { createTest } from "node-harness"; +import { once } from "node:events"; +import { createServer } from "node:http"; +const { expect } = createTest(import.meta.path); + +await using server = createServer().listen(0); +await once(server, "listening"); +fetch(`http://localhost:${server.address().port}`).then(res => res.text()); +const [req, res] = await once(server, "request"); +expect(res.headersSent).toBe(false); +const { promise, resolve } = Promise.withResolvers(); +res.write("first", () => { + res.write("second", () => { + res.end("OK", resolve); + }); +}); +await promise; +expect(res.headersSent).toBe(true); diff --git a/test/js/bun/test/parallel/test-http-req.connection.bytesWritten-must-be-supported-on-the-server.ts b/test/js/bun/test/parallel/test-http-req.connection.bytesWritten-must-be-supported-on-the-server.ts new file mode 100644 index 0000000000..cd0534e2da --- /dev/null +++ b/test/js/bun/test/parallel/test-http-req.connection.bytesWritten-must-be-supported-on-the-server.ts @@ -0,0 +1,28 @@ +import { createTest } from "node-harness"; +import { once } from "node:events"; +import http, { Server } from "node:http"; +import type { AddressInfo } from "node:net"; +const { expect } = createTest(import.meta.path); + +const { promise, resolve } = Promise.withResolvers(); +await using httpServer = http.createServer(function (req, res) { + res.on("finish", () => resolve(req.connection.bytesWritten)); + res.writeHead(200, { "Content-Type": "text/plain" }); + + const chunk = "7".repeat(1024); + const bchunk = Buffer.from(chunk); + res.write(chunk); + res.write(bchunk); + + expect(res.connection.bytesWritten).toBe(1024 * 2); + res.end("bunbunbun"); +}); + +await once(httpServer.listen(0), "listening"); +const address = httpServer.address() as AddressInfo; +const req = http.get({ port: address.port }); +await once(req, "response"); +const bytesWritten = await promise; +expect(typeof bytesWritten).toBe("number"); +expect(bytesWritten).toBe(1024 * 2 + 9); +req.destroy(); diff --git a/test/js/bun/test/parallel/test-http-request-has-the-correct-options.ts b/test/js/bun/test/parallel/test-http-request-has-the-correct-options.ts new file mode 100644 index 0000000000..7e7f5d094c --- /dev/null +++ b/test/js/bun/test/parallel/test-http-request-has-the-correct-options.ts @@ -0,0 +1,9 @@ +import { createTest } from "node-harness"; +import http from "node:http"; +const { expect } = createTest(import.meta.path); + +const { promise, resolve } = Promise.withResolvers(); +http.request("http://google.com/", resolve).end(); +const response = await promise; +expect(response.req.port).toBe(80); +expect(response.req.protocol).toBe("http:"); diff --git a/test/js/bun/test/parallel/test-http-server.listening-should-work.ts b/test/js/bun/test/parallel/test-http-server.listening-should-work.ts new file mode 100644 index 0000000000..8f9e565558 --- /dev/null +++ b/test/js/bun/test/parallel/test-http-server.listening-should-work.ts @@ -0,0 +1,10 @@ +import { createTest } from "node-harness"; +import { once } from "node:events"; +import http from "node:http"; +const { expect } = createTest(import.meta.path); + +const server = http.createServer(); +await once(server.listen(0), "listening"); +expect(server.listening).toBe(true); +server.closeAllConnections(); +expect(server.listening).toBe(false); diff --git a/test/js/bun/test/parallel/test-http-should-accept-custom-certs-when-provided.ts b/test/js/bun/test/parallel/test-http-should-accept-custom-certs-when-provided.ts new file mode 100644 index 0000000000..923d849a5f --- /dev/null +++ b/test/js/bun/test/parallel/test-http-should-accept-custom-certs-when-provided.ts @@ -0,0 +1,34 @@ +import { createTest } from "node-harness"; +import nodefs from "node:fs"; +import https from "node:https"; +import { sep } from "node:path"; +const { expect } = createTest(import.meta.path); + +await using server = https.createServer( + { + key: nodefs.readFileSync( + `${import.meta.dir}/../../../node/http/fixtures/openssl_localhost.key`.replaceAll("/", sep), + ), + cert: nodefs.readFileSync( + `${import.meta.dir}/../../../node/http/fixtures/openssl_localhost.crt`.replaceAll("/", sep), + ), + passphrase: "123123123", + }, + (req, res) => { + res.write("Hello from https server"); + res.end(); + }, +); +server.listen(0, "localhost"); +const address = server.address(); +let url_address = address.address; +const res = await fetch(`https://localhost:${address.port}`, { + tls: { + rejectUnauthorized: true, + ca: nodefs.readFileSync( + `${import.meta.dir}/../../../node/http/fixtures/openssl_localhost_ca.pem`.replaceAll("/", sep), + ), + }, +}); +const t = await res.text(); +expect(t).toEqual("Hello from https server"); diff --git a/test/js/bun/test/parallel/test-http-should-accept-received-and-send-blank-headers.ts b/test/js/bun/test/parallel/test-http-should-accept-received-and-send-blank-headers.ts new file mode 100644 index 0000000000..16b66651e3 --- /dev/null +++ b/test/js/bun/test/parallel/test-http-should-accept-received-and-send-blank-headers.ts @@ -0,0 +1,35 @@ +import { createTest } from "node-harness"; +import { once } from "node:events"; +import http from "node:http"; +import type { AddressInfo } from "node:net"; +import { createConnection } from "node:net"; +const { expect } = createTest(import.meta.path); + +const { promise, resolve, reject } = Promise.withResolvers(); +await using server = http.createServer(async (req, res) => { + expect(req.headers["empty-header"]).toBe(""); + res.writeHead(200, { "x-test": "test", "empty-header": "" }); + res.end(); +}); + +server.listen(0); +await once(server, "listening"); + +const socket = createConnection((server.address() as AddressInfo).port, "localhost", () => { + socket.write( + `GET / HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nConnection: close\r\nEmpty-Header:\r\n\r\n`, + ); +}); + +socket.on("data", data => { + const headers = data.toString("utf-8").split("\r\n"); + expect(headers[0]).toBe("HTTP/1.1 200 OK"); + expect(headers[1]).toBe("x-test: test"); + expect(headers[2]).toBe("empty-header: "); + socket.end(); + resolve(); +}); + +socket.on("error", reject); + +await promise; diff --git a/test/js/bun/test/parallel/test-http-should-allow-Strict-Transport-Security.ts b/test/js/bun/test/parallel/test-http-should-allow-Strict-Transport-Security.ts new file mode 100644 index 0000000000..bd0e852c09 --- /dev/null +++ b/test/js/bun/test/parallel/test-http-should-allow-Strict-Transport-Security.ts @@ -0,0 +1,14 @@ +import { createTest } from "node-harness"; +import { once } from "node:events"; +import http from "node:http"; +const { expect } = createTest(import.meta.path); + +await using server = http.createServer((req, res) => { + res.writeHead(200, { "Strict-Transport-Security": "max-age=31536000" }); + res.end(); +}); +server.listen(0, "localhost"); +await once(server, "listening"); +const response = await fetch(`http://localhost:${server.address().port}`); +expect(response.status).toBe(200); +expect(response.headers.get("strict-transport-security")).toBe("max-age=31536000"); diff --git a/test/js/bun/test/parallel/test-http-should-allow-numbers-headers-to-be-set-in-server-and-client.ts b/test/js/bun/test/parallel/test-http-should-allow-numbers-headers-to-be-set-in-server-and-client.ts new file mode 100644 index 0000000000..a5d571eff5 --- /dev/null +++ b/test/js/bun/test/parallel/test-http-should-allow-numbers-headers-to-be-set-in-server-and-client.ts @@ -0,0 +1,27 @@ +import { createTest } from "node-harness"; +import { once } from "node:events"; +import http from "node:http"; +const { expect } = createTest(import.meta.path); + +let server_headers; +await using server = http.createServer((req, res) => { + server_headers = req.headers; + res.setHeader("x-number", 10); + res.appendHeader("x-number-2", 20); + res.end(); +}); + +await once(server.listen(0, "localhost"), "listening"); +const { promise, resolve } = Promise.withResolvers(); + +{ + const response = http.request(`http://localhost:${server.address().port}`, resolve); + response.setHeader("x-number", 30); + response.appendHeader("x-number-2", 40); + response.end(); +} +const response = (await promise) as Record; +expect(response.headers["x-number"]).toBe("10"); +expect(response.headers["x-number-2"]).toBe("20"); +expect(server_headers["x-number"]).toBe("30"); +expect(server_headers["x-number-2"]).toBe("40"); diff --git a/test/js/bun/test/parallel/test-http-should-be-able-to-flush-headers-socket._httpMessage-must-be-set.ts b/test/js/bun/test/parallel/test-http-should-be-able-to-flush-headers-socket._httpMessage-must-be-set.ts new file mode 100644 index 0000000000..5657d73fa2 --- /dev/null +++ b/test/js/bun/test/parallel/test-http-should-be-able-to-flush-headers-socket._httpMessage-must-be-set.ts @@ -0,0 +1,25 @@ +import { createTest } from "node-harness"; +import { once } from "node:events"; +import http, { Server } from "node:http"; +import type { AddressInfo } from "node:net"; +const { expect } = createTest(import.meta.path); + +await using server = http.createServer((req, res) => { + res.flushHeaders(); +}); + +await once(server.listen(0), "listening"); +const { promise, resolve } = Promise.withResolvers(); +const address = server.address() as AddressInfo; +const req = http.get( + { + hostname: address.address, + port: address.port, + }, + resolve, +); + +const { socket } = req; +await promise; +expect(socket._httpMessage).toBe(req); +socket.destroy(); diff --git a/test/js/bun/test/parallel/test-http-should-emit-clientError-when-Content-Length-is-invalid.ts b/test/js/bun/test/parallel/test-http-should-emit-clientError-when-Content-Length-is-invalid.ts new file mode 100644 index 0000000000..b2779f3e6d --- /dev/null +++ b/test/js/bun/test/parallel/test-http-should-emit-clientError-when-Content-Length-is-invalid.ts @@ -0,0 +1,28 @@ +import { createTest } from "node-harness"; +import { once } from "node:events"; +import http from "node:http"; +import { connect } from "node:net"; +const { expect } = createTest(import.meta.path); + +const { promise, resolve, reject } = Promise.withResolvers(); +await using server = http.createServer(reject); + +server.on("clientError", (err, socket) => { + resolve(err); + socket.destroy(); +}); + +server.listen(0); +await once(server, "listening"); + +const client = connect(server.address().port, () => { + // HTTP request with invalid Content-Length + // The Content-Length says 10 but the actual body is 20 bytes + // Send the request + client.write( + `POST /test HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nContent-Type: text/plain\r\nContent-Length: invalid\r\n\r\n`, + ); +}); + +const err = (await promise) as Error; +expect(err.code).toBe("HPE_UNEXPECTED_CONTENT_LENGTH"); diff --git a/test/js/bun/test/parallel/test-http-should-emit-clientError-when-mixing-Content-Length-and-Transfer-Encoding.ts b/test/js/bun/test/parallel/test-http-should-emit-clientError-when-mixing-Content-Length-and-Transfer-Encoding.ts new file mode 100644 index 0000000000..a1e9e67344 --- /dev/null +++ b/test/js/bun/test/parallel/test-http-should-emit-clientError-when-mixing-Content-Length-and-Transfer-Encoding.ts @@ -0,0 +1,27 @@ +import { createTest } from "node-harness"; +import { once } from "node:events"; +import http from "node:http"; +import { connect } from "node:net"; +const { expect } = createTest(import.meta.path); + +const { promise, resolve, reject } = Promise.withResolvers(); +await using server = http.createServer(reject); + +server.on("clientError", (err, socket) => { + resolve(err); + socket.destroy(); +}); + +await once(server.listen(0), "listening"); + +const client = connect(server.address().port, () => { + // HTTP request with invalid Content-Length + // The Content-Length says 10 but the actual body is 20 bytes + // Send the request + client.write( + `POST /test HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nContent-Type: text/plain\r\nContent-Length: 5\r\nTransfer-Encoding: chunked\r\n\r\nHello`, + ); +}); + +const err = (await promise) as Error; +expect(err.code).toBe("HPE_INVALID_TRANSFER_ENCODING"); diff --git a/test/js/bun/test/parallel/test-http-should-emit-close-when-connection-is-aborted.ts b/test/js/bun/test/parallel/test-http-should-emit-close-when-connection-is-aborted.ts new file mode 100644 index 0000000000..03bd06b0b4 --- /dev/null +++ b/test/js/bun/test/parallel/test-http-should-emit-close-when-connection-is-aborted.ts @@ -0,0 +1,21 @@ +import { createTest } from "node-harness"; +import { once } from "node:events"; +import http from "node:http"; +const { expect } = createTest(import.meta.path); + +await using server = http.createServer().listen(0); +server.unref(); +await once(server, "listening"); +const controller = new AbortController(); +fetch(`http://localhost:${server.address().port}`, { signal: controller.signal }) + .then(res => res.text()) + .catch(() => {}); + +const [req, res] = await once(server, "request"); +const closeEvent = Promise.withResolvers(); +req.once("close", () => { + closeEvent.resolve(); +}); +controller.abort(); +await closeEvent.promise; +expect(req.aborted).toBe(true); diff --git a/test/js/bun/test/parallel/test-http-should-emit-events-in-the-right-order.ts b/test/js/bun/test/parallel/test-http-should-emit-events-in-the-right-order.ts new file mode 100644 index 0000000000..0bf56ece46 --- /dev/null +++ b/test/js/bun/test/parallel/test-http-should-emit-events-in-the-right-order.ts @@ -0,0 +1,33 @@ +import { bunEnv, bunExe } from "harness"; +import { createTest } from "node-harness"; +import * as path from "node:path"; +const { expect } = createTest(import.meta.path); + +const { stdout, exited } = Bun.spawn({ + cmd: [bunExe(), "run", path.join(import.meta.dir, "../../../node/http/fixtures/log-events.mjs")], + stdout: "pipe", + stdin: "ignore", + stderr: "inherit", + env: bunEnv, +}); +const out = await stdout.text(); +// TODO prefinish and socket are not emitted in the right order +expect( + out + .split("\n") + .filter(Boolean) + .map(x => JSON.parse(x)), +).toStrictEqual([ + ["req", "socket"], + ["req", "prefinish"], + ["req", "finish"], + ["req", "response"], + "STATUS: 200", + // TODO: not totally right: + ["res", "resume"], + ["req", "close"], + ["res", "readable"], + ["res", "end"], + ["res", "close"], +]); +expect(await exited).toBe(0); diff --git a/test/js/bun/test/parallel/test-http-should-emit-timeout-event-when-using-server-setTimeout.ts b/test/js/bun/test/parallel/test-http-should-emit-timeout-event-when-using-server-setTimeout.ts new file mode 100644 index 0000000000..cf053bd8db --- /dev/null +++ b/test/js/bun/test/parallel/test-http-should-emit-timeout-event-when-using-server-setTimeout.ts @@ -0,0 +1,23 @@ +import { createTest } from "node-harness"; +import { once } from "node:events"; +import http from "node:http"; +const { expect } = createTest(import.meta.path); + +await using server = http.createServer().listen(0); +await once(server, "listening"); +let callBackCalled = false; +server.setTimeout(100, () => { + callBackCalled = true; + console.log("Called timeout"); +}); + +fetch(`http://localhost:${server.address().port}`, { verbose: true }) + .then(res => res.text()) + .catch(err => { + console.log(err); + }); + +const [req, res] = await once(server, "request"); +expect(req.complete).toBe(false); +await once(server, "timeout"); +expect(callBackCalled).toBe(true); diff --git a/test/js/bun/test/parallel/test-http-should-emit-timeout-event.ts b/test/js/bun/test/parallel/test-http-should-emit-timeout-event.ts new file mode 100644 index 0000000000..d7dda335a1 --- /dev/null +++ b/test/js/bun/test/parallel/test-http-should-emit-timeout-event.ts @@ -0,0 +1,19 @@ +import { createTest } from "node-harness"; +import { once } from "node:events"; +import http from "node:http"; +const { expect } = createTest(import.meta.path); + +await using server = http.createServer().listen(0); +await once(server, "listening"); +fetch(`http://localhost:${server.address().port}`) + .then(res => res.text()) + .catch(() => {}); + +const [req, res] = await once(server, "request"); +expect(req.complete).toBe(false); +let callBackCalled = false; +req.setTimeout(100, () => { + callBackCalled = true; +}); +await once(req, "timeout"); +expect(callBackCalled).toBe(true); diff --git a/test/js/bun/test/parallel/test-http-should-error-with-faulty-args.ts b/test/js/bun/test/parallel/test-http-should-error-with-faulty-args.ts new file mode 100644 index 0000000000..ae347c443b --- /dev/null +++ b/test/js/bun/test/parallel/test-http-should-error-with-faulty-args.ts @@ -0,0 +1,34 @@ +import { createTest } from "node-harness"; +import nodefs from "node:fs"; +import https from "node:https"; +import * as path from "node:path"; +const { expect } = createTest(import.meta.path); + +await using server = https.createServer( + { + key: nodefs.readFileSync(path.join(import.meta.dir, "../../..", "node/http/fixtures", "openssl_localhost.key")), + cert: nodefs.readFileSync(path.join(import.meta.dir, "../../..", "node/http/fixtures", "openssl_localhost.crt")), + passphrase: "123123123", + }, + (req, res) => { + res.write("Hello from https server"); + res.end(); + }, +); +server.listen(0, "localhost"); +const address = server.address(); + +try { + let url_address = address.address; + const res = await fetch(`https://localhost:${address.port}`, { + tls: { + rejectUnauthorized: true, + ca: "some invalid value for a ca", + }, + }); + await res.text(); + expect(true).toBe("unreacheable"); +} catch (err) { + expect(err.code).toBe("FailedToOpenSocket"); + expect(err.message).toBe("Was there a typo in the url or port?"); +} diff --git a/test/js/bun/test/parallel/test-http-should-handle-data-if-not-immediately-handled.ts b/test/js/bun/test/parallel/test-http-should-handle-data-if-not-immediately-handled.ts new file mode 100644 index 0000000000..82978cc039 --- /dev/null +++ b/test/js/bun/test/parallel/test-http-should-handle-data-if-not-immediately-handled.ts @@ -0,0 +1,30 @@ +import { createTest } from "node-harness"; +import { once } from "node:events"; +import http from "node:http"; +const { expect } = createTest(import.meta.path); + +// Create a local server to receive data from +await using server = http.createServer(); + +// Listen to the request event +server.on("request", (request, res) => { + setTimeout(() => { + const body: Uint8Array[] = []; + request.on("data", chunk => { + body.push(chunk); + }); + request.on("end", () => { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(Buffer.concat(body)); + }); + }, 100); +}); +await once(server.listen(0), "listening"); +const url = `http://localhost:${server.address().port}`; +const payload = "Hello, world!".repeat(10).toString(); +const res = await fetch(url, { + method: "POST", + body: payload, +}); +expect(res.status).toBe(200); +expect(await res.text()).toBe(payload); diff --git a/test/js/bun/test/parallel/test-http-should-handle-header-overflow.ts b/test/js/bun/test/parallel/test-http-should-handle-header-overflow.ts new file mode 100644 index 0000000000..1097beab3a --- /dev/null +++ b/test/js/bun/test/parallel/test-http-should-handle-header-overflow.ts @@ -0,0 +1,29 @@ +import { createTest } from "node-harness"; +import { once } from "node:events"; +import http from "node:http"; +import type { AddressInfo } from "node:net"; +import { createConnection } from "node:net"; +const { expect } = createTest(import.meta.path); + +await using server = http.createServer(async (req, res) => { + expect.unreachable(); +}); +const { promise, resolve, reject } = Promise.withResolvers(); +server.on("connection", socket => { + socket.on("error", (err: any) => { + expect(err.code).toBe("HPE_HEADER_OVERFLOW"); + resolve(); + }); +}); +server.listen(0); +await once(server, "listening"); + +const socket = createConnection((server.address() as AddressInfo).port, "localhost", () => { + socket.write( + `GET / HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nConnection: close\r\nBig-Header: ` + + "a".repeat(http.maxHeaderSize) + // will overflow because of host and connection headers + "\r\n\r\n", + ); +}); +socket.on("error", reject); +await promise; diff --git a/test/js/bun/test/parallel/test-http-should-handle-invalid-method.ts b/test/js/bun/test/parallel/test-http-should-handle-invalid-method.ts new file mode 100644 index 0000000000..6d651b60fb --- /dev/null +++ b/test/js/bun/test/parallel/test-http-should-handle-invalid-method.ts @@ -0,0 +1,30 @@ +import http from "node:http"; +import type { AddressInfo } from "node:net"; +import { createConnection } from "node:net"; +import { once } from "node:events"; + +import { createTest } from "node-harness"; +const { expect } = createTest(import.meta.path); + +await using server = http.createServer(async (req, res) => { + expect.unreachable(); +}); +const { promise, resolve, reject } = Promise.withResolvers(); +server.on("connection", socket => { + socket.on("error", (err: any) => { + expect(err.code).toBe("HPE_INVALID_METHOD"); + resolve(); + }); +}); +server.listen(0); +await once(server, "listening"); + +const socket = createConnection((server.address() as AddressInfo).port, "localhost", () => { + socket.write( + `BUN / HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nConnection: close\r\nBig-Header: ` + + "a".repeat(http.maxHeaderSize) + // will overflow because of host and connection headers + "\r\n\r\n", + ); +}); +socket.on("error", reject); +await promise; diff --git a/test/js/bun/test/parallel/test-http-should-not-accept-untrusted-certificates.ts b/test/js/bun/test/parallel/test-http-should-not-accept-untrusted-certificates.ts new file mode 100644 index 0000000000..ad4425d1fd --- /dev/null +++ b/test/js/bun/test/parallel/test-http-should-not-accept-untrusted-certificates.ts @@ -0,0 +1,36 @@ +import { createTest } from "node-harness"; +import nodefs from "node:fs"; +import https from "node:https"; +import * as path from "node:path"; +const { expect } = createTest(import.meta.path); + +await using server = https.createServer( + { + key: nodefs.readFileSync(path.join(import.meta.dir, "../../..", "node/http/fixtures", "openssl.key")), + cert: nodefs.readFileSync(path.join(import.meta.dir, "../../..", "node/http/fixtures", "openssl.crt")), + passphrase: "123123123", + }, + (req, res) => { + res.write("Hello from https server"); + res.end(); + }, +); +server.listen(0, "127.0.0.1"); +const address = server.address(); + +try { + let url_address = address.address; + if (address.family === "IPv6") { + url_address = `[${url_address}]`; + } + const res = await fetch(`https://${url_address}:${address.port}`, { + tls: { + rejectUnauthorized: true, + }, + }); + await res.text(); + expect.unreachable(); +} catch (err) { + expect(err.code).toBe("UNABLE_TO_VERIFY_LEAF_SIGNATURE"); + expect(err.message).toBe("unable to verify the first certificate"); +} diff --git a/test/js/bun/test/parallel/test-http-should-not-emit-or-throw error-when-writing-after-socket.end.ts b/test/js/bun/test/parallel/test-http-should-not-emit-or-throw error-when-writing-after-socket.end.ts new file mode 100644 index 0000000000..4fa9f8442f --- /dev/null +++ b/test/js/bun/test/parallel/test-http-should-not-emit-or-throw error-when-writing-after-socket.end.ts @@ -0,0 +1,30 @@ +import { createTest } from "node-harness"; +import { once } from "node:events"; +import http from "node:http"; +const { expect } = createTest(import.meta.path); + +const { promise, resolve, reject } = Promise.withResolvers(); + +await using server = http.createServer((req, res) => { + res.writeHead(200, { "Connection": "close" }); + + res.socket.end(); + res.on("error", reject); + try { + const result = res.write("Hello, world!"); + resolve(result); + } catch (err) { + reject(err); + } +}); +await once(server.listen(0), "listening"); +const url = `http://localhost:${server.address().port}`; + +await fetch(url, { + method: "POST", + body: Buffer.allocUnsafe(1024 * 1024 * 10), +}) + .then(res => res.bytes()) + .catch(err => {}); + +expect(await promise).toBeTrue(); diff --git a/test/js/bun/test/parallel/test-http-should-reject-non-standard-body-writes-when-rejectNonStandardBodyWrites-is-false.ts b/test/js/bun/test/parallel/test-http-should-reject-non-standard-body-writes-when-rejectNonStandardBodyWrites-is-false.ts new file mode 100644 index 0000000000..e15c1d349a --- /dev/null +++ b/test/js/bun/test/parallel/test-http-should-reject-non-standard-body-writes-when-rejectNonStandardBodyWrites-is-false.ts @@ -0,0 +1,44 @@ +import { createTest } from "node-harness"; +import { once } from "node:events"; +import http from "node:http"; +const { expect } = createTest(import.meta.path); + +let body_not_allowed_on_write; +let body_not_allowed_on_end; + +await using server = http.createServer({ + rejectNonStandardBodyWrites: false, +}); + +server.on("request", (req, res) => { + body_not_allowed_on_write = false; + body_not_allowed_on_end = false; + res.writeHead(204); + + try { + res.write("bun"); + } catch (e: any) { + expect(e?.code).toBe("ERR_HTTP_BODY_NOT_ALLOWED"); + body_not_allowed_on_write = true; + } + try { + res.end("bun"); + } catch (e: any) { + expect(e?.code).toBe("ERR_HTTP_BODY_NOT_ALLOWED"); + body_not_allowed_on_end = true; + // if we throw here, we need to call end() to actually end the request + res.end(); + } +}); + +await once(server.listen(0), "listening"); +const url = `http://localhost:${server.address().port}`; + +{ + await fetch(url, { + method: "GET", + }).then(res => res.text()); + + expect(body_not_allowed_on_write).toBe(false); + expect(body_not_allowed_on_end).toBe(false); +} diff --git a/test/js/bun/test/parallel/test-http-should-reject-non-standard-body-writes-when-rejectNonStandardBodyWrites-is-true.ts b/test/js/bun/test/parallel/test-http-should-reject-non-standard-body-writes-when-rejectNonStandardBodyWrites-is-true.ts new file mode 100644 index 0000000000..d5ee639c8e --- /dev/null +++ b/test/js/bun/test/parallel/test-http-should-reject-non-standard-body-writes-when-rejectNonStandardBodyWrites-is-true.ts @@ -0,0 +1,44 @@ +import { createTest } from "node-harness"; +import { once } from "node:events"; +import http from "node:http"; +const { expect } = createTest(import.meta.path); + +let body_not_allowed_on_write; +let body_not_allowed_on_end; + +await using server = http.createServer({ + rejectNonStandardBodyWrites: true, +}); + +server.on("request", (req, res) => { + body_not_allowed_on_write = false; + body_not_allowed_on_end = false; + res.writeHead(204); + + try { + res.write("bun"); + } catch (e: any) { + expect(e?.code).toBe("ERR_HTTP_BODY_NOT_ALLOWED"); + body_not_allowed_on_write = true; + } + try { + res.end("bun"); + } catch (e: any) { + expect(e?.code).toBe("ERR_HTTP_BODY_NOT_ALLOWED"); + body_not_allowed_on_end = true; + // if we throw here, we need to call end() to actually end the request + res.end(); + } +}); + +await once(server.listen(0), "listening"); +const url = `http://localhost:${server.address().port}`; + +{ + await fetch(url, { + method: "GET", + }).then(res => res.text()); + + expect(body_not_allowed_on_write).toBe(true); + expect(body_not_allowed_on_end).toBe(true); +} diff --git a/test/js/bun/test/parallel/test-http-should-reject-non-standard-body-writes-when-rejectNonStandardBodyWrites-is-undefined.ts b/test/js/bun/test/parallel/test-http-should-reject-non-standard-body-writes-when-rejectNonStandardBodyWrites-is-undefined.ts new file mode 100644 index 0000000000..e24d419dc2 --- /dev/null +++ b/test/js/bun/test/parallel/test-http-should-reject-non-standard-body-writes-when-rejectNonStandardBodyWrites-is-undefined.ts @@ -0,0 +1,44 @@ +import { createTest } from "node-harness"; +import { once } from "node:events"; +import http from "node:http"; +const { expect } = createTest(import.meta.path); + +let body_not_allowed_on_write; +let body_not_allowed_on_end; + +await using server = http.createServer({ + rejectNonStandardBodyWrites: undefined, +}); + +server.on("request", (req, res) => { + body_not_allowed_on_write = false; + body_not_allowed_on_end = false; + res.writeHead(204); + + try { + res.write("bun"); + } catch (e: any) { + expect(e?.code).toBe("ERR_HTTP_BODY_NOT_ALLOWED"); + body_not_allowed_on_write = true; + } + try { + res.end("bun"); + } catch (e: any) { + expect(e?.code).toBe("ERR_HTTP_BODY_NOT_ALLOWED"); + body_not_allowed_on_end = true; + // if we throw here, we need to call end() to actually end the request + res.end(); + } +}); + +await once(server.listen(0), "listening"); +const url = `http://localhost:${server.address().port}`; + +{ + await fetch(url, { + method: "GET", + }).then(res => res.text()); + + expect(body_not_allowed_on_write).toBe(false); + expect(body_not_allowed_on_end).toBe(false); +} diff --git a/test/js/bun/test/parallel/test-http-should-support-localAddress.ts b/test/js/bun/test/parallel/test-http-should-support-localAddress.ts new file mode 100644 index 0000000000..d2015a39b7 --- /dev/null +++ b/test/js/bun/test/parallel/test-http-should-support-localAddress.ts @@ -0,0 +1,33 @@ +import { createTest } from "node-harness"; +import http from "node:http"; +const { expect } = createTest(import.meta.path); + +await new Promise(resolve => { + const server = http.createServer((req, res) => { + const { localAddress, localFamily, localPort } = req.socket; + res.end(); + server.close(); + expect(localAddress).toStartWith("127."); + expect(localFamily).toBe("IPv4"); + expect(localPort).toBeGreaterThan(0); + resolve(); + }); + server.listen(0, "127.0.0.1", () => { + http.request(`http://localhost:${server.address().port}`).end(); + }); +}); + +await new Promise(resolve => { + const server = http.createServer((req, res) => { + const { localAddress, localFamily, localPort } = req.socket; + res.end(); + server.close(); + expect(localAddress).toStartWith("::"); + expect(localFamily).toBe("IPv6"); + expect(localPort).toBeGreaterThan(0); + resolve(); + }); + server.listen(0, "::1", () => { + http.request(`http://[::1]:${server.address().port}`).end(); + }); +}); diff --git a/test/js/bun/test/parallel/test-http-strictContentLength-should-work-on-server.ts b/test/js/bun/test/parallel/test-http-strictContentLength-should-work-on-server.ts new file mode 100644 index 0000000000..01fdcccd79 --- /dev/null +++ b/test/js/bun/test/parallel/test-http-strictContentLength-should-work-on-server.ts @@ -0,0 +1,44 @@ +import { createTest } from "node-harness"; +import { once } from "node:events"; +import http from "node:http"; +const { expect } = createTest(import.meta.path); + +const { promise, resolve, reject } = Promise.withResolvers(); +await using server = http.createServer((req, res) => { + try { + res.strictContentLength = true; + res.writeHead(200, { "Content-Length": 10 }); + + res.write("123456789"); + + // Too much data + try { + res.write("123456789"); + expect.unreachable(); + } catch (e: any) { + expect(e).toBeInstanceOf(Error); + expect(e.code).toBe("ERR_HTTP_CONTENT_LENGTH_MISMATCH"); + } + + // Too little data + try { + res.end(); + expect.unreachable(); + } catch (e: any) { + expect(e).toBeInstanceOf(Error); + expect(e.code).toBe("ERR_HTTP_CONTENT_LENGTH_MISMATCH"); + } + + // Just right + res.end("0"); + resolve(); + } catch (e: any) { + reject(e); + } finally { + } +}); + +await once(server.listen(0), "listening"); +const url = `http://localhost:${server.address().port}`; +await fetch(url, { method: "GET" }).catch(() => {}); +await promise; diff --git a/test/js/node/http/node-http-clientError-uncaughtException-fixture.js b/test/js/bun/test/parallel/test-http-throw-inside-clientError-should-be-propagated-to-uncaughtException.ts similarity index 100% rename from test/js/node/http/node-http-clientError-uncaughtException-fixture.js rename to test/js/bun/test/parallel/test-http-throw-inside-clientError-should-be-propagated-to-uncaughtException.ts diff --git a/test/js/bun/test/parallel/test-http-timeout-destruction-should-be-visible-using-kConnectionsCheckingInterval.ts b/test/js/bun/test/parallel/test-http-timeout-destruction-should-be-visible-using-kConnectionsCheckingInterval.ts new file mode 100644 index 0000000000..70ac11ae68 --- /dev/null +++ b/test/js/bun/test/parallel/test-http-timeout-destruction-should-be-visible-using-kConnectionsCheckingInterval.ts @@ -0,0 +1,10 @@ +import { createTest } from "node-harness"; +import { once } from "node:events"; +import http from "node:http"; +const { expect } = createTest(import.meta.path); + +const { kConnectionsCheckingInterval } = require("_http_server"); +const server = http.createServer(); +await once(server.listen(0), "listening"); +server.closeAllConnections(); +expect(server[kConnectionsCheckingInterval]._destroyed).toBe(true); diff --git a/test/js/bun/test/parallel/test-http-unref-works.ts b/test/js/bun/test/parallel/test-http-unref-works.ts new file mode 100644 index 0000000000..ff5b65b3df --- /dev/null +++ b/test/js/bun/test/parallel/test-http-unref-works.ts @@ -0,0 +1,27 @@ +import { once } from "events"; +import { isWindows } from "harness"; +import { createServer } from "http"; + +if (isWindows) process.exit(0); // Windows doesnt support SIGUSR1 + +const SIGNAL = process.platform === "linux" ? "SIGUSR2" : "SIGUSR1"; +const server = createServer((req, res) => { + res.end(); +}); +server.listen(0); +await once(server, "listening"); +const port = server.address().port; +process.on(SIGNAL, async () => { + server.unref(); + + // check that the server is still running + const resp = await fetch(`http://localhost:${port}`); + await resp.arrayBuffer(); + console.log("Unref'd & server still running (as expected)"); +}); +const resp = await fetch(`http://localhost:${port}`); +await resp.arrayBuffer(); +if (resp.status !== 200) { + process.exit(42); +} +process.kill(process.pid, SIGNAL); diff --git a/test/js/bun/test/parallel/test-https-Agent-is-configured-correctly.ts b/test/js/bun/test/parallel/test-https-Agent-is-configured-correctly.ts new file mode 100644 index 0000000000..2e59b833fd --- /dev/null +++ b/test/js/bun/test/parallel/test-https-Agent-is-configured-correctly.ts @@ -0,0 +1,7 @@ +import { createTest } from "node-harness"; +import https from "node:https"; +const { expect } = createTest(import.meta.path); + +const agent = new https.Agent(); +expect(agent.defaultPort).toBe(443); +expect(agent.protocol).toBe("https:"); diff --git a/test/js/bun/test/parallel/test-https-get-can-use-Agent.ts b/test/js/bun/test/parallel/test-https-get-can-use-Agent.ts new file mode 100644 index 0000000000..a172192cc8 --- /dev/null +++ b/test/js/bun/test/parallel/test-https-get-can-use-Agent.ts @@ -0,0 +1,10 @@ +import { createTest } from "node-harness"; +import https from "node:https"; +const { expect } = createTest(import.meta.path); + +const agent = new https.Agent(); +const { promise, resolve } = Promise.withResolvers(); +https.get({ agent, hostname: "google.com" }, resolve); +const response = await promise; +expect(response.req.port).toBe(443); +expect(response.req.protocol).toBe("https:"); diff --git a/test/js/bun/test/parallel/test-https-req.connection.bytesWritten-must-be-supported-on-the-server.ts b/test/js/bun/test/parallel/test-https-req.connection.bytesWritten-must-be-supported-on-the-server.ts new file mode 100644 index 0000000000..fd91322e30 --- /dev/null +++ b/test/js/bun/test/parallel/test-https-req.connection.bytesWritten-must-be-supported-on-the-server.ts @@ -0,0 +1,33 @@ +import { tls as COMMON_TLS_CERT } from "harness"; +import { createTest } from "node-harness"; +import { once } from "node:events"; +import { Server } from "node:http"; +import https, { createServer as createHttpsServer } from "node:https"; +import type { AddressInfo } from "node:net"; +const { expect } = createTest(import.meta.path); + +const { promise, resolve } = Promise.withResolvers(); +await using httpServer = createHttpsServer(COMMON_TLS_CERT, function (req, res) { + res.on("finish", () => resolve(req.connection.bytesWritten)); + res.writeHead(200, { "Content-Type": "text/plain" }); + + // Write 1.5mb to cause some requests to buffer + // Also, mix up the encodings a bit. + const chunk = "7".repeat(1024); + const bchunk = Buffer.from(chunk); + res.write(chunk); + res.write(bchunk); + // Get .bytesWritten while buffer is not empty + expect(res.connection.bytesWritten).toBe(1024 * 2); + + res.end("bunbunbun"); +}); + +await once(httpServer.listen(0), "listening"); +const address = httpServer.address() as AddressInfo; +const req = https.get({ port: address.port, rejectUnauthorized: false }); +await once(req, "response"); +const bytesWritten = await promise; +expect(typeof bytesWritten).toBe("number"); +expect(bytesWritten).toBe(1024 * 2 + 9); +req.destroy(); diff --git a/test/js/bun/test/parallel/test-https-request-has-the-correct-options.ts b/test/js/bun/test/parallel/test-https-request-has-the-correct-options.ts new file mode 100644 index 0000000000..54e49939a8 --- /dev/null +++ b/test/js/bun/test/parallel/test-https-request-has-the-correct-options.ts @@ -0,0 +1,9 @@ +import { createTest } from "node-harness"; +import https from "node:https"; +const { expect } = createTest(import.meta.path); + +const { promise, resolve } = Promise.withResolvers(); +https.request("https://google.com/", resolve).end(); +const response = await promise; +expect(response.req.port).toBe(443); +expect(response.req.protocol).toBe("https:"); diff --git a/test/js/bun/test/parallel/test-https-should-work-when-sending-request-with-agent-false.ts b/test/js/bun/test/parallel/test-https-should-work-when-sending-request-with-agent-false.ts new file mode 100644 index 0000000000..c9cdb41653 --- /dev/null +++ b/test/js/bun/test/parallel/test-https-should-work-when-sending-request-with-agent-false.ts @@ -0,0 +1,8 @@ +import https from "node:https"; + +const { promise, resolve, reject } = Promise.withResolvers(); +const client = https.request("https://example.com/", { agent: false }); +client.on("error", reject); +client.on("close", resolve); +client.end(); +await promise; diff --git a/test/js/node/http/node-http-ref-fixture.js b/test/js/node/http/node-http-ref-fixture.js deleted file mode 100644 index b5fd7d55d2..0000000000 --- a/test/js/node/http/node-http-ref-fixture.js +++ /dev/null @@ -1,20 +0,0 @@ -import { createServer } from "http"; -const SIGNAL = process.platform === "linux" ? "SIGUSR2" : "SIGUSR1"; -var server = createServer((req, res) => { - res.end(); -}).listen(0, async (err, hostname, port) => { - process.on(SIGNAL, async () => { - server.unref(); - - // check that the server is still running - const resp = await fetch(`http://localhost:${port}`); - await resp.arrayBuffer(); - console.log("Unref'd & server still running (as expected)"); - }); - const resp = await fetch(`http://localhost:${port}`); - await resp.arrayBuffer(); - if (resp.status !== 200) { - process.exit(42); - } - process.kill(process.pid, SIGNAL); -}); diff --git a/test/js/node/http/node-http.test.ts b/test/js/node/http/node-http.test.ts index d882b44d30..c3ea05849b 100644 --- a/test/js/node/http/node-http.test.ts +++ b/test/js/node/http/node-http.test.ts @@ -5,7 +5,7 @@ * * A handful of older tests do not run in Node in this file. These tests should be updated to run in Node, or deleted. */ -import { bunEnv, bunExe, tls as COMMON_TLS_CERT, randomPort } from "harness"; +import { bunEnv, bunExe, randomPort } from "harness"; import { createTest } from "node-harness"; import { spawnSync } from "node:child_process"; import { EventEmitter, once } from "node:events"; @@ -15,7 +15,6 @@ import http, { createServer, get, globalAgent, - IncomingMessage, OutgoingMessage, request, Server, @@ -25,14 +24,13 @@ import http, { } from "node:http"; import https, { createServer as createHttpsServer } from "node:https"; import type { AddressInfo } from "node:net"; -import { connect, createConnection } from "node:net"; +import { connect } from "node:net"; import { tmpdir } from "node:os"; import * as path from "node:path"; -import * as stream from "node:stream"; import { PassThrough } from "node:stream"; -import * as zlib from "node:zlib"; import { run as runHTTPProxyTest } from "./node-http-proxy.js"; const { describe, expect, it, beforeAll, afterAll, createDoneDotAll, mock, test } = createTest(import.meta.path); + function listen(server: Server, protocol: string = "http"): Promise { return new Promise((resolve, reject) => { const timeout = setTimeout(() => reject("Timed out"), 5000).unref(); @@ -1149,6 +1147,7 @@ describe("node:http", () => { expect(err.code).toBe("EADDRINUSE"); }); }); + describe("node https server", async () => { const httpsOptions = { key: nodefs.readFileSync(path.join(import.meta.dir, "fixtures", "cert.key")), @@ -1308,455 +1307,6 @@ describe("server.address should be valid IP", () => { }); }); -it("should not accept untrusted certificates", async () => { - const server = https.createServer( - { - key: nodefs.readFileSync(path.join(import.meta.dir, "fixtures", "openssl.key")), - cert: nodefs.readFileSync(path.join(import.meta.dir, "fixtures", "openssl.crt")), - passphrase: "123123123", - }, - (req, res) => { - res.write("Hello from https server"); - res.end(); - }, - ); - server.listen(0, "127.0.0.1"); - const address = server.address(); - - try { - let url_address = address.address; - if (address.family === "IPv6") { - url_address = `[${url_address}]`; - } - const res = await fetch(`https://${url_address}:${address.port}`, { - tls: { - rejectUnauthorized: true, - }, - }); - await res.text(); - expect(true).toBe("unreacheable"); - } catch (err) { - expect(err.code).toBe("UNABLE_TO_VERIFY_LEAF_SIGNATURE"); - expect(err.message).toBe("unable to verify the first certificate"); - } - - server.close(); -}); - -it("#4415.1 ServerResponse es6", () => { - class Response extends ServerResponse { - constructor(req) { - super(req); - } - } - const req = {}; - const res = new Response(req); - expect(res.req).toBe(req); -}); - -it("#4415.2 ServerResponse es5", () => { - function Response(req) { - ServerResponse.call(this, req); - } - Response.prototype = Object.create(ServerResponse.prototype); - const req = {}; - const res = new Response(req); - expect(res.req).toBe(req); -}); - -it("#4415.3 Server es5", done => { - const server = Server((req, res) => { - res.end(); - }); - server.listen(0, async (_err, host, port) => { - try { - const res = await fetch(`http://localhost:${port}`); - expect(res.status).toBe(200); - done(); - } catch (err) { - done(err); - } finally { - server.close(); - } - }); -}); - -it("#4415.4 IncomingMessage es5", done => { - // This matches Node.js: - const im = Object.create(IncomingMessage.prototype); - IncomingMessage.call(im, { url: "/foo" }); - expect(im.url).toBe(""); - - let didCall = false; - function Subclass(...args) { - IncomingMessage.apply(this, args); - didCall = true; - } - Object.setPrototypeOf(Subclass.prototype, IncomingMessage.prototype); - Object.setPrototypeOf(Subclass, IncomingMessage); - - const server = new Server( - { - IncomingMessage: Subclass, - }, - (req, res) => { - if (req instanceof Subclass && didCall) { - expect(req.url).toBe("/foo"); - res.writeHead(200, { "Content-Type": "text/plain" }); - res.end("hello"); - } else { - res.writeHead(500, { "Content-Type": "text/plain" }); - res.end("bye"); - } - }, - ); - server.listen(0, () => { - fetch(`http://localhost:${server.address().port}/foo`, { - method: "GET", - }).then(response => { - expect(response.status).toBe(200); - server.close(done); - }); - }); -}); - -it("#9242.1 Server has constructor", () => { - const s = new Server(); - expect(s.constructor).toBe(Server); -}); -it("#9242.2 IncomingMessage has constructor", () => { - const im = new IncomingMessage("http://localhost"); - expect(im.constructor).toBe(IncomingMessage); -}); -it("#9242.3 OutgoingMessage has constructor", () => { - const om = new OutgoingMessage(); - expect(om.constructor).toBe(OutgoingMessage); -}); -it("#9242.4 ServerResponse has constructor", () => { - const sr = new ServerResponse({}); - expect(sr.constructor).toBe(ServerResponse); -}); - -// Windows doesnt support SIGUSR1 -if (process.platform !== "win32") { - // By not timing out, this test passes. - test(".unref() works", async () => { - expect([path.join(import.meta.dir, "node-http-ref-fixture.js")]).toRun(); - }); -} - -it("#10177 response.write with non-ascii latin1 should not cause duplicated character or segfault", () => { - // this can cause a segfault so we run it in a separate process - const { exitCode } = Bun.spawnSync({ - cmd: [bunExe(), "run", path.join(import.meta.dir, "node-http-response-write-encode-fixture.js")], - env: bunEnv, - stdout: "inherit", - stderr: "inherit", - }); - expect(exitCode).toBe(0); -}, 60_000); - -it("#11425 http no payload limit", done => { - const server = Server((req, res) => { - res.end(); - }); - server.listen(0, async (_err, host, port) => { - try { - const res = await fetch(`http://localhost:${port}`, { - method: "POST", - body: new Uint8Array(1024 * 1024 * 200), - }); - expect(res.status).toBe(200); - done(); - } catch (err) { - done(err); - } finally { - server.close(); - } - }); -}); - -it("should emit events in the right order", async () => { - const { stdout, exited } = Bun.spawn({ - cmd: [bunExe(), "run", path.join(import.meta.dir, "fixtures/log-events.mjs")], - stdout: "pipe", - stdin: "ignore", - stderr: "inherit", - env: bunEnv, - }); - const out = await stdout.text(); - // TODO prefinish and socket are not emitted in the right order - expect( - out - .split("\n") - .filter(Boolean) - .map(x => JSON.parse(x)), - ).toStrictEqual([ - ["req", "socket"], - ["req", "prefinish"], - ["req", "finish"], - ["req", "response"], - "STATUS: 200", - // TODO: not totally right: - ["res", "resume"], - ["req", "close"], - ["res", "readable"], - ["res", "end"], - ["res", "close"], - ]); - expect(await exited).toBe(0); -}); - -it("destroy should end download", async () => { - // just simulate some file that will take forever to download - const payload = Buffer.alloc(128 * 1024, "X"); - for (let i = 0; i < 5; i++) { - let sendedByteLength = 0; - using server = Bun.serve({ - port: 0, - async fetch(req) { - let running = true; - req.signal.onabort = () => (running = false); - return new Response(async function* () { - while (running) { - sendedByteLength += payload.byteLength; - yield payload; - await Bun.sleep(10); - } - }); - }, - }); - - async function run() { - let receivedByteLength = 0; - let { promise, resolve } = Promise.withResolvers(); - const req = request(server.url, res => { - res.on("data", data => { - receivedByteLength += data.length; - if (resolve) { - resolve(); - resolve = null; - } - }); - }); - req.end(); - await promise; - req.destroy(); - await Bun.sleep(10); - const initialByteLength = receivedByteLength; - // we should receive the same amount of data we sent - expect(initialByteLength).toBeLessThanOrEqual(sendedByteLength); - await Bun.sleep(10); - // we should not receive more data after destroy - expect(initialByteLength).toBe(receivedByteLength); - await Bun.sleep(10); - } - - const runCount = 50; - const runs = Array.from({ length: runCount }, run); - await Promise.all(runs); - Bun.gc(true); - await Bun.sleep(10); - } -}); - -it("can send brotli from Server and receive with fetch", async () => { - try { - var server = createServer((req, res) => { - expect(req.url).toBe("/hello"); - res.writeHead(200); - res.setHeader("content-encoding", "br"); - - const inputStream = new stream.Readable(); - inputStream.push("Hello World"); - inputStream.push(null); - - inputStream.pipe(zlib.createBrotliCompress()).pipe(res); - }); - const url = await listen(server); - const res = await fetch(new URL("/hello", url)); - expect(await res.text()).toBe("Hello World"); - } catch (e) { - throw e; - } finally { - server.close(); - } -}); - -it("can send gzip from Server and receive with fetch", async () => { - try { - var server = createServer((req, res) => { - expect(req.url).toBe("/hello"); - res.writeHead(200); - res.setHeader("content-encoding", "gzip"); - - const inputStream = new stream.Readable(); - inputStream.push("Hello World"); - inputStream.push(null); - - inputStream.pipe(zlib.createGzip()).pipe(res); - }); - const url = await listen(server); - const res = await fetch(new URL("/hello", url)); - expect(await res.text()).toBe("Hello World"); - } catch (e) { - throw e; - } finally { - server.close(); - } -}); - -it("can send deflate from Server and receive with fetch", async () => { - try { - var server = createServer((req, res) => { - expect(req.url).toBe("/hello"); - res.writeHead(200); - res.setHeader("content-encoding", "deflate"); - - const inputStream = new stream.Readable(); - inputStream.push("Hello World"); - inputStream.push(null); - - inputStream.pipe(zlib.createDeflate()).pipe(res); - }); - const url = await listen(server); - const res = await fetch(new URL("/hello", url)); - expect(await res.text()).toBe("Hello World"); - } catch (e) { - throw e; - } finally { - server.close(); - } -}); - -it("can send brotli from Server and receive with Client", async () => { - try { - var server = createServer((req, res) => { - expect(req.url).toBe("/hello"); - res.writeHead(200); - res.setHeader("content-encoding", "br"); - - const inputStream = new stream.Readable(); - inputStream.push("Hello World"); - inputStream.push(null); - - const passthrough = new stream.PassThrough(); - passthrough.on("data", data => res.write(data)); - passthrough.on("end", () => res.end()); - - inputStream.pipe(zlib.createBrotliCompress()).pipe(passthrough); - }); - - const url = await listen(server); - const { resolve, reject, promise } = Promise.withResolvers(); - http.get(new URL("/hello", url), res => { - let rawData = ""; - const passthrough = stream.PassThrough(); - passthrough.on("data", chunk => { - rawData += chunk; - }); - passthrough.on("end", () => { - try { - expect(Buffer.from(rawData)).toEqual(Buffer.from("Hello World")); - resolve(); - } catch (e) { - reject(e); - } - }); - res.pipe(zlib.createBrotliDecompress()).pipe(passthrough); - }); - await promise; - } catch (e) { - throw e; - } finally { - server.close(); - } -}); - -it("ServerResponse ClientRequest field exposes agent getter", async () => { - try { - var server = createServer((req, res) => { - expect(req.url).toBe("/hello"); - res.writeHead(200); - res.end("world"); - }); - const url = await listen(server); - const { resolve, reject, promise } = Promise.withResolvers(); - http.get(new URL("/hello", url), res => { - try { - expect(res.req.agent.protocol).toBe("http:"); - resolve(); - } catch (e) { - reject(e); - } - }); - await promise; - } catch (e) { - throw e; - } finally { - server.close(); - } -}); - -it("should accept custom certs when provided", async () => { - const server = https.createServer( - { - key: nodefs.readFileSync(path.join(import.meta.dir, "fixtures", "openssl_localhost.key")), - cert: nodefs.readFileSync(path.join(import.meta.dir, "fixtures", "openssl_localhost.crt")), - passphrase: "123123123", - }, - (req, res) => { - res.write("Hello from https server"); - res.end(); - }, - ); - server.listen(0, "localhost"); - const address = server.address(); - - let url_address = address.address; - const res = await fetch(`https://localhost:${address.port}`, { - tls: { - rejectUnauthorized: true, - ca: nodefs.readFileSync(path.join(import.meta.dir, "fixtures", "openssl_localhost_ca.pem")), - }, - }); - const t = await res.text(); - expect(t).toEqual("Hello from https server"); - - server.close(); -}); -it("should error with faulty args", async () => { - const server = https.createServer( - { - key: nodefs.readFileSync(path.join(import.meta.dir, "fixtures", "openssl_localhost.key")), - cert: nodefs.readFileSync(path.join(import.meta.dir, "fixtures", "openssl_localhost.crt")), - passphrase: "123123123", - }, - (req, res) => { - res.write("Hello from https server"); - res.end(); - }, - ); - server.listen(0, "localhost"); - const address = server.address(); - - try { - let url_address = address.address; - const res = await fetch(`https://localhost:${address.port}`, { - tls: { - rejectUnauthorized: true, - ca: "some invalid value for a ca", - }, - }); - await res.text(); - expect(true).toBe("unreacheable"); - } catch (err) { - expect(err.code).toBe("FailedToOpenSocket"); - expect(err.message).toBe("Was there a typo in the url or port?"); - } - server.close(); -}); - it("should propagate exception in sync data handler", async () => { const { exitCode, stdout } = Bun.spawnSync({ cmd: [bunExe(), "run", path.join(import.meta.dir, "node-http-error-in-data-handler-fixture.1.js")], @@ -1827,1259 +1377,6 @@ it.skip("should be able to stream huge amounts of data", async () => { } }, 30_000); -// TODO: today we use a workaround to continue event, we need to fix it in the future. -it("should emit continue event #7480", done => { - let receivedContinue = false; - const req = https.request( - "https://example.com", - { headers: { "accept-encoding": "identity", "expect": "100-continue" } }, - res => { - let data = ""; - res.setEncoding("utf8"); - res.on("data", chunk => { - data += chunk; - }); - res.on("end", () => { - expect(receivedContinue).toBe(true); - expect(data).toContain("This domain is for use in illustrative examples in documents"); - done(); - }); - res.on("error", err => done(err)); - }, - ); - req.on("continue", () => { - receivedContinue = true; - }); - req.end(); -}); - -it("should not emit continue event #7480", done => { - let receivedContinue = false; - const req = https.request("https://example.com", { headers: { "accept-encoding": "identity" } }, res => { - let data = ""; - res.setEncoding("utf8"); - res.on("data", chunk => { - data += chunk; - }); - res.on("end", () => { - expect(receivedContinue).toBe(false); - expect(data).toContain("This domain is for use in illustrative examples in documents"); - done(); - }); - res.on("error", err => done(err)); - }); - req.on("continue", () => { - receivedContinue = true; - }); - req.end(); -}); - -it("http.Agent is configured correctly", () => { - const agent = new http.Agent(); - expect(agent.defaultPort).toBe(80); - expect(agent.protocol).toBe("http:"); -}); - -it("https.Agent is configured correctly", () => { - const agent = new https.Agent(); - expect(agent.defaultPort).toBe(443); - expect(agent.protocol).toBe("https:"); -}); - -it("http.get can use http.Agent", async () => { - const agent = new http.Agent(); - const { promise, resolve } = Promise.withResolvers(); - http.get({ agent, hostname: "google.com" }, resolve); - const response = await promise; - expect(response.req.port).toBe(80); - expect(response.req.protocol).toBe("http:"); -}); - -it("https.get can use https.Agent", async () => { - const agent = new https.Agent(); - const { promise, resolve } = Promise.withResolvers(); - https.get({ agent, hostname: "google.com" }, resolve); - const response = await promise; - expect(response.req.port).toBe(443); - expect(response.req.protocol).toBe("https:"); -}); - -it("http.request has the correct options", async () => { - const { promise, resolve } = Promise.withResolvers(); - http.request("http://google.com/", resolve).end(); - const response = await promise; - expect(response.req.port).toBe(80); - expect(response.req.protocol).toBe("http:"); -}); - -it("https.request has the correct options", async () => { - const { promise, resolve } = Promise.withResolvers(); - https.request("https://google.com/", resolve).end(); - const response = await promise; - expect(response.req.port).toBe(443); - expect(response.req.protocol).toBe("https:"); -}); - -it("using node:http to do https: request fails", () => { - expect(() => http.request("https://example.com")).toThrow(TypeError); - expect(() => http.request("https://example.com")).toThrow({ - code: "ERR_INVALID_PROTOCOL", - message: `Protocol "https:" not supported. Expected "http:"`, - }); -}); - -it("should emit close, and complete should be true only after close #13373", async () => { - const server = http.createServer().listen(0); - try { - await once(server, "listening"); - fetch(`http://localhost:${server.address().port}`) - .then(res => res.text()) - .catch(() => {}); - - const [req, res] = await once(server, "request"); - expect(req.complete).toBe(false); - console.log("ok 1"); - const closeEvent = once(req, "close"); - res.end("hi"); - - await closeEvent; - expect(req.complete).toBe(true); - } finally { - server.closeAllConnections(); - } -}); - -it("should emit close when connection is aborted", async () => { - const server = http.createServer().listen(0); - server.unref(); - try { - await once(server, "listening"); - const controller = new AbortController(); - fetch(`http://localhost:${server.address().port}`, { signal: controller.signal }) - .then(res => res.text()) - .catch(() => {}); - - const [req, res] = await once(server, "request"); - const closeEvent = Promise.withResolvers(); - req.once("close", () => { - closeEvent.resolve(); - }); - controller.abort(); - await closeEvent.promise; - expect(req.aborted).toBe(true); - } finally { - server.close(); - } -}); - -it("should emit timeout event", async () => { - const server = http.createServer().listen(0); - try { - await once(server, "listening"); - fetch(`http://localhost:${server.address().port}`) - .then(res => res.text()) - .catch(() => {}); - - const [req, res] = await once(server, "request"); - expect(req.complete).toBe(false); - let callBackCalled = false; - req.setTimeout(100, () => { - callBackCalled = true; - }); - await once(req, "timeout"); - expect(callBackCalled).toBe(true); - } finally { - server.closeAllConnections(); - } -}, 12_000); - -it("should emit timeout event when using server.setTimeout", async () => { - const server = http.createServer().listen(0); - try { - await once(server, "listening"); - let callBackCalled = false; - server.setTimeout(100, () => { - callBackCalled = true; - console.log("Called timeout"); - }); - - fetch(`http://localhost:${server.address().port}`, { verbose: true }) - .then(res => res.text()) - .catch(err => { - console.log(err); - }); - - const [req, res] = await once(server, "request"); - expect(req.complete).toBe(false); - await once(server, "timeout"); - expect(callBackCalled).toBe(true); - } finally { - server.closeAllConnections(); - } -}, 12_000); - -it("must set headersSent to true after headers are sent #3458", async () => { - const server = createServer().listen(0); - try { - await once(server, "listening"); - fetch(`http://localhost:${server.address().port}`).then(res => res.text()); - const [req, res] = await once(server, "request"); - expect(res.headersSent).toBe(false); - const { promise, resolve } = Promise.withResolvers(); - res.end("OK", resolve); - await promise; - expect(res.headersSent).toBe(true); - } finally { - server.close(); - } -}); - -it("must set headersSent to true after headers are sent when using chunk encoded", async () => { - const server = createServer().listen(0); - try { - await once(server, "listening"); - fetch(`http://localhost:${server.address().port}`).then(res => res.text()); - const [req, res] = await once(server, "request"); - expect(res.headersSent).toBe(false); - const { promise, resolve } = Promise.withResolvers(); - res.write("first", () => { - res.write("second", () => { - res.end("OK", resolve); - }); - }); - await promise; - expect(res.headersSent).toBe(true); - } finally { - server.close(); - } -}); - -it("should work when sending https.request with agent:false", async () => { - const { promise, resolve, reject } = Promise.withResolvers(); - const client = https.request("https://example.com/", { agent: false }); - client.on("error", reject); - client.on("close", resolve); - client.end(); - await promise; -}); - -it("client should use chunked encoding if more than one write is called", async () => { - function sleep(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); - } - // Bun.serve is used here until #15576 or similar fix is merged - using server = Bun.serve({ - port: 0, - hostname: "127.0.0.1", - fetch(req) { - if (req.headers.get("transfer-encoding") !== "chunked") { - return new Response("should be chunked encoding", { status: 500 }); - } - return new Response(req.body); - }, - }); - - // Options for the HTTP request - const options = { - hostname: "127.0.0.1", // Replace with the target server - port: server.port, - path: "/api/data", - method: "POST", - headers: { - "Content-Type": "application/json", - }, - }; - - const { promise, resolve, reject } = Promise.withResolvers(); - - // Create the request - const req = http.request(options, res => { - if (res.statusCode !== 200) { - reject(new Error("Body should be chunked")); - } - const chunks = []; - // Collect the response data - res.on("data", chunk => { - chunks.push(chunk); - }); - - res.on("end", () => { - resolve(chunks); - }); - }); - - // Handle errors - req.on("error", reject); - - // Write chunks to the request body - - for (let i = 0; i < 4; i++) { - req.write("chunk"); - await sleep(50); - req.write(" "); - await sleep(50); - } - req.write("BUN!"); - // End the request and signal no more data will be sent - req.end(); - - const chunks = await promise; - expect(chunks.length).toBeGreaterThan(1); - expect(chunks[chunks.length - 1]?.toString()).toEndWith("BUN!"); - expect(Buffer.concat(chunks).toString()).toBe("chunk ".repeat(4) + "BUN!"); -}); - -it("client should use content-length if only one write is called", async () => { - await using server = http.createServer((req, res) => { - if (req.headers["transfer-encoding"] === "chunked") { - return res.writeHead(500).end(); - } - res.writeHead(200); - req.on("data", data => { - res.write(data); - }); - req.on("end", () => { - res.end(); - }); - }); - - await once(server.listen(0, "127.0.0.1"), "listening"); - - // Options for the HTTP request - const options = { - hostname: "127.0.0.1", // Replace with the target server - port: server.address().port, - path: "/api/data", - method: "POST", - headers: { - "Content-Type": "application/json", - }, - }; - - const { promise, resolve, reject } = Promise.withResolvers(); - - // Create the request - const req = http.request(options, res => { - if (res.statusCode !== 200) { - reject(new Error("Body should not be chunked")); - } - const chunks = []; - // Collect the response data - res.on("data", chunk => { - chunks.push(chunk); - }); - - res.on("end", () => { - resolve(chunks); - }); - }); - // Handle errors - req.on("error", reject); - // Write chunks to the request body - req.write("Hello World BUN!"); - // End the request and signal no more data will be sent - req.end(); - - const chunks = await promise; - expect(chunks.length).toBe(1); - expect(chunks[0]?.toString()).toBe("Hello World BUN!"); - expect(Buffer.concat(chunks).toString()).toBe("Hello World BUN!"); -}); - -it("should allow numbers headers to be set in node:http server and client", async () => { - let server_headers; - await using server = http.createServer((req, res) => { - server_headers = req.headers; - res.setHeader("x-number", 10); - res.appendHeader("x-number-2", 20); - res.end(); - }); - - await once(server.listen(0, "localhost"), "listening"); - const { promise, resolve } = Promise.withResolvers(); - - { - const response = http.request(`http://localhost:${server.address().port}`, resolve); - response.setHeader("x-number", 30); - response.appendHeader("x-number-2", 40); - response.end(); - } - const response = (await promise) as Record; - expect(response.headers["x-number"]).toBe("10"); - expect(response.headers["x-number-2"]).toBe("20"); - expect(server_headers["x-number"]).toBe("30"); - expect(server_headers["x-number-2"]).toBe("40"); -}); - -it("should allow Strict-Transport-Security when using node:http", async () => { - await using server = http.createServer((req, res) => { - res.writeHead(200, { "Strict-Transport-Security": "max-age=31536000" }); - res.end(); - }); - server.listen(0, "localhost"); - await once(server, "listening"); - const response = await fetch(`http://localhost:${server.address().port}`); - expect(response.status).toBe(200); - expect(response.headers.get("strict-transport-security")).toBe("max-age=31536000"); -}); - -it("should support localAddress", async () => { - await new Promise(resolve => { - const server = http.createServer((req, res) => { - const { localAddress, localFamily, localPort } = req.socket; - res.end(); - server.close(); - expect(localAddress).toStartWith("127."); - expect(localFamily).toBe("IPv4"); - expect(localPort).toBeGreaterThan(0); - resolve(); - }); - server.listen(0, "127.0.0.1", () => { - http.request(`http://localhost:${server.address().port}`).end(); - }); - }); - - await new Promise(resolve => { - const server = http.createServer((req, res) => { - const { localAddress, localFamily, localPort } = req.socket; - res.end(); - server.close(); - expect(localAddress).toStartWith("::"); - expect(localFamily).toBe("IPv6"); - expect(localPort).toBeGreaterThan(0); - resolve(); - }); - server.listen(0, "::1", () => { - http.request(`http://[::1]:${server.address().port}`).end(); - }); - }); -}); - -it("should not emit/throw error when writing after socket.end", async () => { - const { promise, resolve, reject } = Promise.withResolvers(); - - const server = http.createServer((req, res) => { - res.writeHead(200, { "Connection": "close" }); - - res.socket.end(); - res.on("error", reject); - try { - const result = res.write("Hello, world!"); - resolve(result); - } catch (err) { - reject(err); - } - }); - try { - await once(server.listen(0), "listening"); - const url = `http://localhost:${server.address().port}`; - - await fetch(url, { - method: "POST", - body: Buffer.allocUnsafe(1024 * 1024 * 10), - }) - .then(res => res.bytes()) - .catch(err => {}); - - expect(await promise).toBeTrue(); - } finally { - server.close(); - } -}); - -it("should handle data if not immediately handled", async () => { - // Create a local server to receive data from - const server = http.createServer(); - - // Listen to the request event - server.on("request", (request, res) => { - setTimeout(() => { - const body: Uint8Array[] = []; - request.on("data", chunk => { - body.push(chunk); - }); - request.on("end", () => { - res.writeHead(200, { "Content-Type": "application/json" }); - res.end(Buffer.concat(body)); - }); - }, 100); - }); - try { - await once(server.listen(0), "listening"); - const url = `http://localhost:${server.address().port}`; - const payload = "Hello, world!".repeat(10).toString(); - const res = await fetch(url, { - method: "POST", - body: payload, - }); - expect(res.status).toBe(200); - expect(await res.text()).toBe(payload); - } finally { - server.close(); - } -}); - -it("Empty requests should not be Transfer-Encoding: chunked", async () => { - const server = http.createServer((req, res) => { - res.end(JSON.stringify(req.headers)); - }); - await once(server.listen(0), "listening"); - const url = `http://localhost:${server.address().port}`; - try { - for (let method of ["GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"]) { - const { promise, resolve, reject } = Promise.withResolvers(); - http - .request( - url, - { - method, - }, - res => { - const body: Uint8Array[] = []; - res.on("data", chunk => { - body.push(chunk); - }); - res.on("end", () => { - try { - resolve(JSON.parse(Buffer.concat(body).toString())); - } catch (e) { - reject(e); - } - }); - }, - ) - .on("error", reject) - .end(); - - const headers = (await promise) as Record; - expect(headers).toBeDefined(); - expect(headers["transfer-encoding"]).toBeUndefined(); - switch (method) { - case "GET": - case "DELETE": - case "OPTIONS": - // Content-Length will not be present for GET, DELETE, and OPTIONS - // aka DELETE in node.js will be undefined and in bun it will be 0 - // this is not outside the spec but is different between node.js and bun - expect(headers["content-length"]).toBeOneOf(["0", undefined]); - break; - default: - expect(headers["content-length"]).toBeDefined(); - break; - } - } - } finally { - server.close(); - } -}); - -it("should reject non-standard body writes when rejectNonStandardBodyWrites is true", async () => { - { - let body_not_allowed_on_write; - let body_not_allowed_on_end; - - for (const rejectNonStandardBodyWrites of [true, false, undefined]) { - await using server = http.createServer({ - rejectNonStandardBodyWrites, - }); - - server.on("request", (req, res) => { - body_not_allowed_on_write = false; - body_not_allowed_on_end = false; - res.writeHead(204); - - try { - res.write("bun"); - } catch (e: any) { - expect(e?.code).toBe("ERR_HTTP_BODY_NOT_ALLOWED"); - body_not_allowed_on_write = true; - } - try { - res.end("bun"); - } catch (e: any) { - expect(e?.code).toBe("ERR_HTTP_BODY_NOT_ALLOWED"); - body_not_allowed_on_end = true; - // if we throw here, we need to call end() to actually end the request - res.end(); - } - }); - - await once(server.listen(0), "listening"); - const url = `http://localhost:${server.address().port}`; - - { - await fetch(url, { - method: "GET", - }).then(res => res.text()); - - expect(body_not_allowed_on_write).toBe(rejectNonStandardBodyWrites || false); - expect(body_not_allowed_on_end).toBe(rejectNonStandardBodyWrites || false); - } - } - } -}); - -test("should emit clientError when Content-Length is invalid", async () => { - const { promise, resolve, reject } = Promise.withResolvers(); - await using server = http.createServer(reject); - - server.on("clientError", (err, socket) => { - resolve(err); - socket.destroy(); - }); - - server.listen(0); - await once(server, "listening"); - - const client = connect(server.address().port, () => { - // HTTP request with invalid Content-Length - // The Content-Length says 10 but the actual body is 20 bytes - // Send the request - client.write( - `POST /test HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nContent-Type: text/plain\r\nContent-Length: invalid\r\n\r\n`, - ); - }); - - const err = (await promise) as Error; - expect(err.code).toBe("HPE_UNEXPECTED_CONTENT_LENGTH"); -}); - -test("should emit clientError when mixing Content-Length and Transfer-Encoding", async () => { - const { promise, resolve, reject } = Promise.withResolvers(); - await using server = http.createServer(reject); - - server.on("clientError", (err, socket) => { - resolve(err); - socket.destroy(); - }); - - await once(server.listen(0), "listening"); - - const client = connect(server.address().port, () => { - // HTTP request with invalid Content-Length - // The Content-Length says 10 but the actual body is 20 bytes - // Send the request - client.write( - `POST /test HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nContent-Type: text/plain\r\nContent-Length: 5\r\nTransfer-Encoding: chunked\r\n\r\nHello`, - ); - }); - - const err = (await promise) as Error; - expect(err.code).toBe("HPE_INVALID_TRANSFER_ENCODING"); -}); - -test("should be able to flush headers socket._httpMessage must be set", async () => { - let server: Server | undefined; - try { - server = http.createServer((req, res) => { - res.flushHeaders(); - }); - - await once(server.listen(0), "listening"); - const { promise, resolve } = Promise.withResolvers(); - const address = server.address() as AddressInfo; - const req = http.get( - { - hostname: address.address, - port: address.port, - }, - resolve, - ); - - const { socket } = req; - await promise; - expect(socket._httpMessage).toBe(req); - socket.destroy(); - } finally { - server?.closeAllConnections(); - } -}); - -test("req.connection.bytesWritten must be supported on the server", async () => { - let httpServer: Server; - try { - const { promise, resolve } = Promise.withResolvers(); - httpServer = http.createServer(function (req, res) { - res.on("finish", () => resolve(req.connection.bytesWritten)); - res.writeHead(200, { "Content-Type": "text/plain" }); - - const chunk = "7".repeat(1024); - const bchunk = Buffer.from(chunk); - res.write(chunk); - res.write(bchunk); - - expect(res.connection.bytesWritten).toBe(1024 * 2); - res.end("bunbunbun"); - }); - - await once(httpServer.listen(0), "listening"); - const address = httpServer.address() as AddressInfo; - const req = http.get({ port: address.port }); - await once(req, "response"); - const bytesWritten = await promise; - expect(typeof bytesWritten).toBe("number"); - expect(bytesWritten).toBe(1024 * 2 + 9); - req.destroy(); - } finally { - httpServer?.closeAllConnections(); - } -}); - -test("req.connection.bytesWritten must be supported on the https server", async () => { - let httpServer: Server; - try { - const { promise, resolve } = Promise.withResolvers(); - httpServer = createHttpsServer(COMMON_TLS_CERT, function (req, res) { - res.on("finish", () => resolve(req.connection.bytesWritten)); - res.writeHead(200, { "Content-Type": "text/plain" }); - - // Write 1.5mb to cause some requests to buffer - // Also, mix up the encodings a bit. - const chunk = "7".repeat(1024); - const bchunk = Buffer.from(chunk); - res.write(chunk); - res.write(bchunk); - // Get .bytesWritten while buffer is not empty - expect(res.connection.bytesWritten).toBe(1024 * 2); - - res.end("bunbunbun"); - }); - - await once(httpServer.listen(0), "listening"); - const address = httpServer.address() as AddressInfo; - const req = https.get({ port: address.port, rejectUnauthorized: false }); - await once(req, "response"); - const bytesWritten = await promise; - expect(typeof bytesWritten).toBe("number"); - expect(bytesWritten).toBe(1024 * 2 + 9); - req.destroy(); - } finally { - httpServer?.closeAllConnections(); - } -}); - -test("host array should throw in http.request", () => { - expect(() => - http.request({ - host: [1, 2, 3], - }), - ).toThrow('The "options.host" property must be of type string, undefined, or null. Received an instance of Array'); -}); - -test("strictContentLength should work on server", async () => { - const { promise, resolve, reject } = Promise.withResolvers(); - await using server = http.createServer((req, res) => { - try { - res.strictContentLength = true; - res.writeHead(200, { "Content-Length": 10 }); - - res.write("123456789"); - - // Too much data - try { - res.write("123456789"); - expect.unreachable(); - } catch (e: any) { - expect(e).toBeInstanceOf(Error); - expect(e.code).toBe("ERR_HTTP_CONTENT_LENGTH_MISMATCH"); - } - - // Too little data - try { - res.end(); - expect.unreachable(); - } catch (e: any) { - expect(e).toBeInstanceOf(Error); - expect(e.code).toBe("ERR_HTTP_CONTENT_LENGTH_MISMATCH"); - } - - // Just right - res.end("0"); - resolve(); - } catch (e: any) { - reject(e); - } finally { - } - }); - - await once(server.listen(0), "listening"); - const url = `http://localhost:${server.address().port}`; - await fetch(url, { - method: "GET", - }).catch(() => {}); - await promise; -}); - -test("client side flushHeaders should work", async () => { - const { promise, resolve } = Promise.withResolvers(); - await using server = http.createServer((req, res) => { - resolve(req.headers); - res.end(); - }); - - await once(server.listen(0), "listening"); - const address = server.address() as AddressInfo; - const req = http.request({ - method: "GET", - host: "127.0.0.1", - port: address.port, - }); - req.setHeader("foo", "bar"); - req.flushHeaders(); - const headers = await promise; - expect(headers).toBeDefined(); - expect(headers.foo).toEqual("bar"); -}); - -test("flushHeaders should not drop request body", async () => { - const { promise, resolve } = Promise.withResolvers(); - await using server = http.createServer((req, res) => { - let body = ""; - req.setEncoding("utf8"); - req.on("data", chunk => (body += chunk)); - req.on("end", () => { - resolve(body); - res.end(); - }); - }); - - await once(server.listen(0), "listening"); - const address = server.address() as AddressInfo; - const req = http.request({ - method: "POST", - host: "127.0.0.1", - port: address.port, - headers: { "content-type": "text/plain" }, - }); - - req.flushHeaders(); - req.write("bun"); - req.end("rocks"); - - const body = await promise; - expect(body).toBe("bunrocks"); -}); - -test("server.listening should work", async () => { - const server = http.createServer(); - await once(server.listen(0), "listening"); - expect(server.listening).toBe(true); - server.closeAllConnections(); - expect(server.listening).toBe(false); -}); - -test("asyncDispose should work in http.Server", async () => { - const server = http.createServer(); - await once(server.listen(0), "listening"); - expect(server.listening).toBe(true); - await server[Symbol.asyncDispose](); - expect(server.listening).toBe(false); -}); - -test("timeout destruction should be visible using kConnectionsCheckingInterval", async () => { - const { kConnectionsCheckingInterval } = require("_http_server"); - const server = http.createServer(); - await once(server.listen(0), "listening"); - server.closeAllConnections(); - expect(server[kConnectionsCheckingInterval]._destroyed).toBe(true); -}); - -test("client should be able to send a array of [key, value] as headers", async () => { - const { promise, resolve } = Promise.withResolvers(); - await using server = http.createServer((req, res) => { - resolve([req, res]); - }); - await once(server.listen(0), "listening"); - const address = server.address() as AddressInfo; - http.get({ - host: "127.0.0.1", - port: address.port, - headers: [ - ["foo", "bar"], - ["foo", "baz"], - ["host", "127.0.0.1"], - ["host", "127.0.0.2"], - ["host", "127.0.0.3"], - ], - }); - - const [req, res] = await promise; - expect(req.headers.foo).toBe("bar, baz"); - expect(req.headers.host).toBe("127.0.0.1"); - - res.end(); -}); - -test("clientError should fire when receiving invalid method", async () => { - await using server = http.createServer((req, res) => { - res.end(); - }); - let socket; - server.on("clientError", err => { - expect(err.code).toBe("HPE_INVALID_METHOD"); - expect(err.rawPacket.toString()).toBe("*"); - - socket.end(); - }); - await once(server.listen(0), "listening"); - const address = server.address() as AddressInfo; - socket = createConnection({ port: address.port }); - - await once(socket, "connect"); - socket.write("*"); - await once(socket, "close"); -}); - -test("throw inside clientError should be propagated to uncaughtException", async () => { - const testFile = path.join(import.meta.dir, "node-http-clientError-uncaughtException-fixture.js"); - expect([testFile]).toRun("", 0); -}); - -test("chunked encoding must be valid after flushHeaders", async () => { - const { promise, resolve, reject } = Promise.withResolvers(); - await using server = http.createServer(async (req, res) => { - res.writeHead(200, { "Content-Type": "text/plain", "Transfer-Encoding": "chunked" }); - res.flushHeaders(); - // make sure headers are flushed - await Bun.sleep(10); - // send some chunks at once - res.write("chunk 1"); - res.write("chunk 2"); - res.write("chunk 3"); - res.write("chunk 4"); - res.write("chunk 5"); - await Bun.sleep(10); - // send some more chunk - res.write("chunk 6"); - res.write("chunk 7"); - await Bun.sleep(10); - // send the last chunk - res.end(); - }); - - server.listen(0); - await once(server, "listening"); - - const socket = connect(server.address().port, () => { - socket.write(`GET / HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nConnection: close\r\n\r\n`); - }); - - const chunks = []; - let received_headers = false; - socket.on("data", data => { - if (!received_headers) { - received_headers = true; - const headers = data.toString("utf-8").split("\r\n"); - expect(headers[0]).toBe("HTTP/1.1 200 OK"); - expect(headers[1]).toBe("Content-Type: text/plain"); - expect(headers[2]).toBe("Transfer-Encoding: chunked"); - expect(headers[3].startsWith("Date:")).toBe(true); - // empty line for end of headers aka flushHeaders works - expect(headers[headers.length - 1]).toBe(""); - expect(headers[headers.length - 2]).toBe(""); - } else { - chunks.push(data); - } - }); - - function parseChunkedData(buffer) { - let offset = 0; - let result = Buffer.alloc(0); - - while (offset < buffer.length) { - // Find the CRLF that terminates the chunk size line - let lineEnd = buffer.indexOf("\r\n", offset); - if (lineEnd === -1) break; - - // Parse the chunk size (in hex) - const chunkSizeHex = buffer.toString("ascii", offset, lineEnd); - const chunkSize = parseInt(chunkSizeHex, 16); - expect(isNaN(chunkSize)).toBe(false); - // If chunk size is 0, we've reached the end - if (chunkSize === 0) { - // Skip the final CRLF after the 0-size chunk - offset = lineEnd + 4; - break; - } - - // Move past the chunk size line's CRLF - offset = lineEnd + 2; - - // Extract the chunk data - const chunkData = buffer.slice(offset, offset + chunkSize); - - // Concatenate this chunk to our result - result = Buffer.concat([result, chunkData]); - - // Move past this chunk's data and its terminating CRLF - offset += chunkSize + 2; - } - - return result; - } - - socket.on("end", () => { - try { - const body = parseChunkedData(Buffer.concat(chunks)); - expect(body.toString("utf-8")).toBe("chunk 1chunk 2chunk 3chunk 4chunk 5chunk 6chunk 7"); - resolve(); - } catch (e) { - reject(e); - } finally { - socket.end(); - } - }); - await promise; -}); - -test("chunked encoding must be valid using minimal code", async () => { - const { promise, resolve, reject } = Promise.withResolvers(); - await using server = http.createServer(async (req, res) => { - res.writeHead(200, { "Content-Type": "text/plain", "Transfer-Encoding": "chunked" }); - res.write("chunk 1"); - res.end("chunk 2"); - }); - - server.listen(0); - await once(server, "listening"); - - const socket = connect(server.address().port, () => { - socket.write(`GET / HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nConnection: close\r\n\r\n`); - }); - - const chunks = []; - socket.on("data", data => { - chunks.push(data); - }); - - function parseChunkedData(buffer) { - let offset = 0; - let result = Buffer.alloc(0); - - while (offset < buffer.length) { - // Find the CRLF that terminates the chunk size line - let lineEnd = buffer.indexOf("\r\n", offset); - if (lineEnd === -1) break; - - // Parse the chunk size (in hex) - const chunkSizeHex = buffer.toString("ascii", offset, lineEnd); - const chunkSize = parseInt(chunkSizeHex, 16); - expect(isNaN(chunkSize)).toBe(false); - // If chunk size is 0, we've reached the end - if (chunkSize === 0) { - // Skip the final CRLF after the 0-size chunk - offset = lineEnd + 4; - break; - } - - // Move past the chunk size line's CRLF - offset = lineEnd + 2; - - // Extract the chunk data - const chunkData = buffer.slice(offset, offset + chunkSize); - - // Concatenate this chunk to our result - result = Buffer.concat([result, chunkData]); - - // Move past this chunk's data and its terminating CRLF - offset += chunkSize + 2; - } - - return result; - } - - socket.on("end", () => { - try { - const data = Buffer.concat(chunks); - - const headersEnd = data.indexOf("\r\n\r\n"); - const headers = data.toString("utf-8", 0, headersEnd).split("\r\n"); - expect(headers[0]).toBe("HTTP/1.1 200 OK"); - expect(headers[1]).toBe("Content-Type: text/plain"); - expect(headers[2]).toBe("Transfer-Encoding: chunked"); - expect(headers[3].startsWith("Date:")).toBe(true); - const body = parseChunkedData(data.slice(headersEnd + 4)); - expect(body.toString("utf-8")).toBe("chunk 1chunk 2"); - resolve(); - } catch (e) { - reject(e); - } finally { - socket.end(); - } - }); - await promise; -}); - -test("chunked encoding must be valid after without flushHeaders", async () => { - const { promise, resolve, reject } = Promise.withResolvers(); - await using server = http.createServer(async (req, res) => { - res.writeHead(200, { "Content-Type": "text/plain", "Transfer-Encoding": "chunked" }); - // send some chunks at once - res.write("chunk 1"); - res.write("chunk 2"); - res.write("chunk 3"); - res.write("chunk 4"); - res.write("chunk 5"); - await Bun.sleep(10); - // send some more chunk - res.write("chunk 6"); - res.write("chunk 7"); - await Bun.sleep(10); - // send the last chunk - res.end(); - }); - - server.listen(0); - await once(server, "listening"); - - const socket = connect(server.address().port, () => { - socket.write(`GET / HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nConnection: close\r\n\r\n`); - }); - - const chunks = []; - socket.on("data", data => { - chunks.push(data); - }); - - function parseChunkedData(buffer) { - let offset = 0; - let result = Buffer.alloc(0); - - while (offset < buffer.length) { - // Find the CRLF that terminates the chunk size line - let lineEnd = buffer.indexOf("\r\n", offset); - if (lineEnd === -1) break; - - // Parse the chunk size (in hex) - const chunkSizeHex = buffer.toString("ascii", offset, lineEnd); - const chunkSize = parseInt(chunkSizeHex, 16); - expect(isNaN(chunkSize)).toBe(false); - // If chunk size is 0, we've reached the end - if (chunkSize === 0) { - // Skip the final CRLF after the 0-size chunk - offset = lineEnd + 4; - break; - } - - // Move past the chunk size line's CRLF - offset = lineEnd + 2; - - // Extract the chunk data - const chunkData = buffer.slice(offset, offset + chunkSize); - - // Concatenate this chunk to our result - result = Buffer.concat([result, chunkData]); - - // Move past this chunk's data and its terminating CRLF - offset += chunkSize + 2; - } - - return result; - } - - socket.on("end", () => { - try { - const data = Buffer.concat(chunks); - - const headersEnd = data.indexOf("\r\n\r\n"); - const headers = data.toString("utf-8", 0, headersEnd).split("\r\n"); - expect(headers[0]).toBe("HTTP/1.1 200 OK"); - expect(headers[1]).toBe("Content-Type: text/plain"); - expect(headers[2]).toBe("Transfer-Encoding: chunked"); - expect(headers[3].startsWith("Date:")).toBe(true); - const body = parseChunkedData(data.slice(headersEnd + 4)); - expect(body.toString("utf-8")).toBe("chunk 1chunk 2chunk 3chunk 4chunk 5chunk 6chunk 7"); - resolve(); - } catch (e) { - reject(e); - } finally { - socket.end(); - } - }); - await promise; -}); - -test("should accept received and send blank headers", async () => { - const { promise, resolve, reject } = Promise.withResolvers(); - await using server = http.createServer(async (req, res) => { - expect(req.headers["empty-header"]).toBe(""); - res.writeHead(200, { "x-test": "test", "empty-header": "" }); - res.end(); - }); - - server.listen(0); - await once(server, "listening"); - - const socket = createConnection((server.address() as AddressInfo).port, "localhost", () => { - socket.write( - `GET / HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nConnection: close\r\nEmpty-Header:\r\n\r\n`, - ); - }); - - socket.on("data", data => { - const headers = data.toString("utf-8").split("\r\n"); - expect(headers[0]).toBe("HTTP/1.1 200 OK"); - expect(headers[1]).toBe("x-test: test"); - expect(headers[2]).toBe("empty-header: "); - socket.end(); - resolve(); - }); - - socket.on("error", reject); - - await promise; -}); - -test("should handle header overflow", async () => { - await using server = http.createServer(async (req, res) => { - expect.unreachable(); - }); - const { promise, resolve, reject } = Promise.withResolvers(); - server.on("connection", socket => { - socket.on("error", (err: any) => { - expect(err.code).toBe("HPE_HEADER_OVERFLOW"); - resolve(); - }); - }); - server.listen(0); - await once(server, "listening"); - - const socket = createConnection((server.address() as AddressInfo).port, "localhost", () => { - socket.write( - `GET / HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nConnection: close\r\nBig-Header: ` + - "a".repeat(http.maxHeaderSize) + // will overflow because of host and connection headers - "\r\n\r\n", - ); - }); - socket.on("error", reject); - await promise; -}); - -test("should handle invalid method", async () => { - await using server = http.createServer(async (req, res) => { - expect.unreachable(); - }); - const { promise, resolve, reject } = Promise.withResolvers(); - server.on("connection", socket => { - socket.on("error", (err: any) => { - expect(err.code).toBe("HPE_INVALID_METHOD"); - resolve(); - }); - }); - server.listen(0); - await once(server, "listening"); - - const socket = createConnection((server.address() as AddressInfo).port, "localhost", () => { - socket.write( - `BUN / HTTP/1.1\r\nHost: localhost:${server.address().port}\r\nConnection: close\r\nBig-Header: ` + - "a".repeat(http.maxHeaderSize) + // will overflow because of host and connection headers - "\r\n\r\n", - ); - }); - socket.on("error", reject); - await promise; -}); - describe("HTTP Server Security Tests - Advanced", () => { // Setup and teardown utilities let server; diff --git a/test/no-validate-leaksan.txt b/test/no-validate-leaksan.txt index 6fb30c85fc..217b98d0dc 100644 --- a/test/no-validate-leaksan.txt +++ b/test/no-validate-leaksan.txt @@ -145,6 +145,7 @@ test/js/node/test/parallel/test-tls-fast-writing.js test/js/bun/sqlite/sqlite.test.js test/js/workerd/html-rewriter.test.js test/regression/issue/12250.test.ts +test/js/bun/test/parallel/test-http-10177-response.write-with-non-ascii-latin1-should-not-cause-duplicated-character-or-segfault.ts # crash for reasons not related to LSAN