// node --test js/http-datachannel.test.mjs // httpChannel against Node's HTTP server, a fake datachannel bridged to a TCP socket (as webrtc-peer and // the guest agent bridge a tag): content-length, chunked and close-delimited bodies, keep-alive on one // connection, Connection: close ending it, and the size cap. import assert from "node:assert/strict"; import { createServer } from "node:http"; import { connect, createServer as createTcp } from "node:net"; import { after, test } from "node:test"; import { BODY_MAX, httpChannel } from "./http-datachannel.js"; const connections = []; const http = createServer((req, res) => { let body = ""; req.on("data", (d) => (body += d)).on("end", () => { if (req.url === "/length") return res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify({ got: body, host: req.headers.host })); if (req.url === "/chunked") return (res.write("a".repeat(20000)), res.write("é"), res.end("z")); if (req.url === "/close") return res.writeHead(200, { connection: "close" }).end("bye"); if (req.url === "/empty") return res.writeHead(202).end(); if (req.url === "/huge") return res.writeHead(200, { "content-length": BODY_MAX + 1 }).end(); }); }).on("connection", (s) => connections.push(s)).listen(0, "127.0.0.1"); // A raw server for a response delimited by the close (HTTP/1.0 style, no length). const raw = createTcp((s) => s.once("data", () => s.end("HTTP/1.0 200 OK\r\ncontent-type: text/plain\r\n\r\nuntil close"))).listen(0, "127.0.0.1"); await Promise.all([http, raw].map((s) => new Promise((r) => s.once("listening", r)))); after(() => (http.close(), raw.close(), connections.forEach((s) => s.destroy()))); // A datachannel whose other end is 127.0.0.1:port. async function channel(server) { const socket = connect(server.address().port, "127.0.0.1"); await new Promise((r) => socket.once("connect", r)); const dc = { readyState: "open", send: (bytes) => socket.write(bytes), close: () => socket.end(), }; socket.on("data", (d) => dc.onmessage?.({ data: d.buffer.slice(d.byteOffset, d.byteOffset + d.length) })); socket.on("close", () => ((dc.readyState = "closed"), dc.onclose?.())); return dc; } const text = (r) => new TextDecoder().decode(r.body); test("keep-alive: several requests on one channel, content-length and chunked bodies", async () => { const before = connections.length; const conn = httpChannel(await channel(http)); const big = "x".repeat(50000); const one = await conn.request({ path: "/length", headers: { "content-type": "application/json" }, body: big }); assert.deepEqual([one.status, JSON.parse(text(one))], [200, { got: big, host: "guest" }]); assert.equal(conn.reusable, true); const two = await conn.request({ path: "/chunked" }); assert.equal(two.headers["transfer-encoding"], "chunked"); assert.equal(text(two), `${"a".repeat(20000)}éz`); const three = await conn.request({ path: "/empty" }); assert.deepEqual([three.status, three.body.length, conn.reusable], [202, 0, true]); assert.equal(connections.length - before, 1, "one connection"); }); test("Connection: close and a close-delimited body end the channel", async () => { const conn = httpChannel(await channel(http)); assert.equal(text(await conn.request({ path: "/close" })), "bye"); assert.equal(conn.reusable, false); const old = httpChannel(await channel(raw)); assert.equal(text(await old.request({ method: "GET", path: "/" })), "until close"); assert.equal(old.reusable, false); }); test("a body over BODY_MAX is refused and the channel closed", async () => { const conn = httpChannel(await channel(http)); await assert.rejects(conn.request({ path: "/huge" }), /over 4194304 bytes/); assert.equal(conn.reusable, false); });