| 1 | describe("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 | |
| 81 | describe("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 | |
| 200 | import { strict as assert } from "node:assert"; |
| 201 | import { once } from "node:events"; |
| 202 | import * as nodeHttp from "node:http"; |
| 203 | import * as net from "node:net"; |
| 204 | import * as consumers from "node:stream/consumers"; |
| 205 | import { describe, test, vi } from "vitest"; |
| 206 | import * as http from "./http.ts"; |