1describe("parseRangeHeader", () => {
2 test("closed, open, and suffix ranges", () => {
3 assert.deepEqual(http.parseRangeHeader("bytes=0-1", 100), [{ start: 0, end: 1 }]);
4 assert.deepEqual(http.parseRangeHeader("bytes=10-", 100), [{ start: 10, end: 99 }]);
5 assert.deepEqual(http.parseRangeHeader("bytes=-10", 100), [{ start: 90, end: 99 }]);
6 assert.deepEqual(http.parseRangeHeader("bytes=-500", 100), [{ start: 0, end: 99 }]);
7 assert.deepEqual(http.parseRangeHeader("bytes=50-500", 100), [{ start: 50, end: 99 }]);
8 assert.deepEqual(http.parseRangeHeader("bytes=0-9, 20-29", 100), [
9 { start: 0, end: 9 },
10 { start: 20, end: 29 },
11 ]);
12 });
13
14 test("ranges past the end are unsatisfiable", () => {
15 // https://shale.paperclover.net/sitegen/issues/106
16 assert.deepEqual(http.parseRangeHeader("bytes=46226599-", 25705639), []);
17 assert.deepEqual(http.parseRangeHeader("bytes=100-200", 100), []);
18 assert.deepEqual(http.parseRangeHeader("bytes=-0", 100), []);
19 assert.deepEqual(http.parseRangeHeader("bytes=0-", 0), []);
20 });
21
22 test("malformed headers are ignored", () => {
23 for (
24 const header of [
25 "",
26 "bytes",
27 "bytes=",
28 "bytes=-",
29 "bytes=5-2",
30 "bytes=a-b",
31 "bytes=1.5-3",
32 "bytes=0x10-",
33 "bytes=--5",
34 "bytes=1-2-3",
35 "pages=0-1",
36 "bytes=0-5,3-9",
37 ]
38 ) {
39 assert.equal(http.parseRangeHeader(header, 100), null, header);
40 }
41 });
42
43 test("fuzz: every range it returns can be read", () => {
44 const pieces = [
45 "bytes",
46 "=",
47 "-",
48 ",",
49 " ",
50 "0",
51 "1",
52 "7",
53 "99",
54 "100",
55 "101",
56 "25705639",
57 "9".repeat(30),
58 "x",
59 ".",
60 "e3",
61 "-1",
62 ];
63 let state = 106;
64 const random = (max: number) => (state = (state * 48271) % 0x7fffffff) % max;
65 for (let i = 0; i < 20_000; i++) {
66 const size = [0, 1, 100, 25705639][random(4)]!;
67 let header = random(4) === 0 ? "" : "bytes=";
68 for (let n = random(7); n > 0; n--) header += pieces[random(pieces.length)];
69
70 for (const { start, end } of http.parseRangeHeader(header, size) ?? []) {
71 assert.ok(
72 Number.isInteger(start) && Number.isInteger(end)
73 && 0 <= start && start <= end && end < size,
74 `${JSON.stringify(header)} of ${size} gave [${start}, ${end}]`,
75 );
76 }
77 }
78 });
79});
80
81describe("serve", () => {
82 test("a client that disconnects cancels the response body", async () => {
83 const cancelled = Promise.withResolvers<void>();
84 await using server = await http.serve({
85 port: 0,
86 respond: async () =>
87 new Response(
88 new ReadableStream({
89 pull: (controller) => controller.enqueue(new Uint8Array(65536)),
90 cancel: () => cancelled.resolve(),
91 }),
92 ),
93 });
94
95 const response = await new Promise<nodeHttp.IncomingMessage>((resolve) => nodeHttp.get(server.url, resolve));
96 response.once("data", () => response.destroy());
97 await cancelled.promise;
98 });
99
100 test("a client that disconnects before the response is ready cancels its body", async () => {
101 const logged = vi.spyOn(console, "error").mockImplementation(() => {});
102 const asked = Promise.withResolvers<void>();
103 const gone = Promise.withResolvers<void>();
104 const cancelled = Promise.withResolvers<void>();
105 await using server = await http.serve({
106 port: 0,
107 respond: async () => {
108 asked.resolve();
109 await gone.promise;
110 return new Response(new ReadableStream({ cancel: () => cancelled.resolve() }));
111 },
112 });
113 const client = net.connect(server.port, "127.0.0.1");
114 client.write("GET / HTTP/1.1\r\nhost: localhost\r\n\r\n");
115 await asked.promise;
116 client.destroy();
117 setTimeout(gone.resolve, 100);
118 await cancelled.promise;
119 assert.deepEqual(logged.mock.calls, []);
120 logged.mockRestore();
121 });
122
123 test("a failing response body closes the connection", async () => {
124 const failure = Object.assign(new Error("bad file descriptor"), { code: "EBADF" });
125 const logged = vi.spyOn(console, "error").mockImplementation(() => {});
126 let pulls = 0;
127 await using server = await http.serve({
128 port: 0,
129 respond: async () =>
130 new Response(
131 new ReadableStream({
132 pull(controller) {
133 if (pulls++ === 0) controller.enqueue(new Uint8Array(16));
134 else controller.error(failure);
135 },
136 }),
137 { headers: { "Content-Length": "65536" } },
138 ),
139 });
140
141 await assert.rejects(async () => {
142 const response = await new Promise<nodeHttp.IncomingMessage>((resolve, reject) =>
143 nodeHttp.get(server.url, resolve).on("error", reject)
144 );
145 await consumers.buffer(response);
146 }, { code: "ECONNRESET" });
147 assert.deepEqual(logged.mock.calls, [[failure]]);
148 logged.mockRestore();
149 });
150
151 test("closing ends the connections that switched protocols", async () => {
152 const server = await http.serve({
153 port: 0,
154 respond: async () => new Response(""),
155 upgrade: (_, socket) => void socket.write("HTTP/1.1 101 Switching Protocols\r\nconnection: Upgrade\r\n\r\n"),
156 });
157 const client = net.connect(server.port, "127.0.0.1");
158 client.write("GET / HTTP/1.1\r\nhost: localhost\r\nconnection: Upgrade\r\nupgrade: test\r\n\r\n");
159 await once(client, "data");
160 const closed = once(client, "close");
161 await server.close();
162 await closed;
163 });
164
165 test("a request to switch protocols that shouldUpgrade declines is responded to like any other", async () => {
166 await using server = await http.serve({
167 port: 0,
168 respond: async (request) => new Response(`${request.method} ${await request.text()}`),
169 upgrade: (_, socket) => void socket.end("HTTP/1.1 403 Forbidden\r\n\r\n"),
170 shouldUpgrade: (request) => request.headers.get("upgrade") === "websocket",
171 });
172 const client = net.connect(server.port, "127.0.0.1");
173 client.write(
174 "POST / HTTP/1.1\r\nhost: localhost\r\nconnection: Upgrade, close\r\nupgrade: h2c\r\n"
175 + "content-length: 5\r\n\r\nhello",
176 );
177 const response = await consumers.text(client);
178 assert.match(response, /^HTTP\/1\.1 200 /);
179 assert.match(response, /POST hello/);
180 });
181
182 test("an upgrade that throws closes its connection, and the server goes on", async () => {
183 const logged = vi.spyOn(console, "error").mockImplementation(() => {});
184 await using server = await http.serve({
185 port: 0,
186 respond: async () => new Response("ok"),
187 upgrade: () => {
188 throw new Error("no");
189 },
190 });
191 const client = net.connect(server.port, "127.0.0.1");
192 client.write("GET / HTTP/1.1\r\nhost: localhost\r\nconnection: Upgrade\r\nupgrade: test\r\n\r\n");
193 await once(client, "close");
194 assert.equal(await (await fetch(server.url)).text(), "ok");
195 assert.equal(logged.mock.calls.length, 1);
196 logged.mockRestore();
197 });
198});
199
200import { strict as assert } from "node:assert";
201import { once } from "node:events";
202import * as nodeHttp from "node:http";
203import * as net from "node:net";
204import * as consumers from "node:stream/consumers";
205import { describe, test, vi } from "vitest";
206import * as http from "./http.ts";