// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. // deno-lint-ignore-file no-deprecated-deno-api import { Buffer, BufReader, BufWriter } from "@std/io/mod.ts"; import { TextProtoReader } from "../testdata/run/textproto.ts"; import { assert, assertEquals, assertRejects, assertStrictEquals, assertThrows, delay, fail, } from "./test_util.ts"; import { join } from "@std/path/mod.ts"; const listenPort = 4507; const listenPort2 = 4508; const { buildCaseInsensitiveCommaValueFinder, // @ts-expect-error TypeScript (as of 3.7) does not support indexing namespaces by symbol } = Deno[Deno.internal]; async function writeRequestAndReadResponse(conn: Deno.Conn): Promise { const encoder = new TextEncoder(); const decoder = new TextDecoder(); const w = new BufWriter(conn); const r = new BufReader(conn); const body = `GET / HTTP/1.1\r\nHost: 127.0.0.1:${listenPort}\r\n\r\n`; const writeResult = await w.write(encoder.encode(body)); assertEquals(body.length, writeResult); await w.flush(); const tpr = new TextProtoReader(r); const statusLine = await tpr.readLine(); assert(statusLine !== null); const headers = await tpr.readMimeHeader(); assert(headers !== null); const chunkedReader = chunkedBodyReader(headers, r); const buf = new Uint8Array(5); const dest = new Buffer(); let result: number | null; while ((result = await chunkedReader.read(buf)) !== null) { const len = Math.min(buf.byteLength, result); await dest.write(buf.subarray(0, len)); } return decoder.decode(dest.bytes()); } Deno.test({ permissions: { net: true } }, async function httpServerBasic() { let httpConn: Deno.HttpConn; const promise = (async () => { const listener = Deno.listen({ port: listenPort }); const conn = await listener.accept(); listener.close(); httpConn = Deno.serveHttp(conn); const reqEvent = await httpConn.nextRequest(); assert(reqEvent); const { request, respondWith } = reqEvent; assertEquals(new URL(request.url).href, `http://127.0.0.1:${listenPort}/`); assertEquals(await request.text(), ""); await respondWith( new Response("Hello World", { headers: { "foo": "bar" } }), ); })(); const resp = await fetch(`http://127.0.0.1:${listenPort}/`, { headers: { "connection": "close" }, }); const clone = resp.clone(); const text = await resp.text(); assertEquals(text, "Hello World"); assertEquals(resp.headers.get("foo"), "bar"); const cloneText = await clone.text(); assertEquals(cloneText, "Hello World"); await promise; httpConn!.close(); }); // https://github.com/denoland/deno/issues/15107 Deno.test( { permissions: { net: true } }, async function httpLazyHeadersIssue15107() { let headers: Headers; const promise = (async () => { const listener = Deno.listen({ port: 2333 }); const conn = await listener.accept(); listener.close(); const httpConn = Deno.serveHttp(conn); const e = await httpConn.nextRequest(); assert(e); const { request } = e; request.text(); headers = request.headers; httpConn!.close(); })(); const conn = await Deno.connect({ port: 2333 }); // Send GET request with a body + content-length. const encoder = new TextEncoder(); const body = `GET / HTTP/1.1\r\nHost: 127.0.0.1:2333\r\nContent-Length: 5\r\n\r\n12345`; const writeResult = await conn.write(encoder.encode(body)); assertEquals(body.length, writeResult); await promise; conn.close(); assertEquals(headers!.get("content-length"), "5"); }, ); Deno.test( { permissions: { net: true } }, async function httpReadHeadersAfterClose() { const promise = (async () => { const listener = Deno.listen({ port: 2334 }); const conn = await listener.accept(); listener.close(); const httpConn = Deno.serveHttp(conn); const e = await httpConn.nextRequest(); assert(e); const { request, respondWith } = e; await request.text(); // Read body await respondWith(new Response("Hello World")); // Closes request assertThrows(() => request.headers, TypeError, "request closed"); httpConn!.close(); })(); const conn = await Deno.connect({ port: 2334 }); // Send GET request with a body + content-length. const encoder = new TextEncoder(); const body = `GET / HTTP/1.1\r\nHost: 127.0.0.1:2333\r\nContent-Length: 5\r\n\r\n12345`; const writeResult = await conn.write(encoder.encode(body)); assertEquals(body.length, writeResult); await promise; conn.close(); }, ); Deno.test( { permissions: { net: true } }, async function httpServerGetRequestBody() { let httpConn: Deno.HttpConn; const promise = (async () => { const listener = Deno.listen({ port: listenPort }); const conn = await listener.accept(); listener.close(); httpConn = Deno.serveHttp(conn); const e = await httpConn.nextRequest(); assert(e); const { request, respondWith } = e; assertEquals(request.body, null); await respondWith(new Response("", { headers: {} })); })(); const conn = await Deno.connect({ port: listenPort }); // Send GET request with a body + content-length. const encoder = new TextEncoder(); const body = `GET / HTTP/1.1\r\nHost: 127.0.0.1:${listenPort}\r\nContent-Length: 5\r\n\r\n12345`; const writeResult = await conn.write(encoder.encode(body)); assertEquals(body.length, writeResult); const resp = new Uint8Array(200); const readResult = await conn.read(resp); assertEquals(readResult, 138); conn.close(); await promise; httpConn!.close(); }, ); Deno.test( { permissions: { net: true } }, async function httpServerStreamResponse() { const stream = new TransformStream(); const writer = stream.writable.getWriter(); writer.write(new TextEncoder().encode("hello ")); writer.write(new TextEncoder().encode("world")); writer.close(); let httpConn: Deno.HttpConn; const listener = Deno.listen({ port: listenPort }); const promise = (async () => { const conn = await listener.accept(); httpConn = Deno.serveHttp(conn); const evt = await httpConn.nextRequest(); assert(evt); const { request, respondWith } = evt; assert(!request.body); await respondWith(new Response(stream.readable)); })(); const resp = await fetch(`http://127.0.0.1:${listenPort}/`); const respBody = await resp.text(); assertEquals("hello world", respBody); await promise; httpConn!.close(); listener.close(); }, ); Deno.test( { permissions: { net: true } }, async function httpServerStreamRequest() { const stream = new TransformStream(); const writer = stream.writable.getWriter(); writer.write(new TextEncoder().encode("hello ")); writer.write(new TextEncoder().encode("world")); writer.close(); const listener = Deno.listen({ port: listenPort }); const promise = (async () => { const conn = await listener.accept(); const httpConn = Deno.serveHttp(conn); const evt = await httpConn.nextRequest(); assert(evt); const { request, respondWith } = evt; const reqBody = await request.text(); assertEquals("hello world", reqBody); await respondWith(new Response("")); // TODO(ry) If we don't call httpConn.nextRequest() here we get "error sending // request for url (https://localhost:${listenPort}/): connection closed before // message completed". assertEquals(await httpConn.nextRequest(), null); listener.close(); })(); const resp = await fetch(`http://127.0.0.1:${listenPort}/`, { body: stream.readable, method: "POST", headers: { "connection": "close" }, }); await resp.body?.cancel(); await promise; }, ); Deno.test( { permissions: { net: true } }, async function httpServerStreamDuplex() { let httpConn: Deno.HttpConn; const listener = Deno.listen({ port: listenPort }); const promise = (async () => { const conn = await listener.accept(); httpConn = Deno.serveHttp(conn); const evt = await httpConn.nextRequest(); assert(evt); const { request, respondWith } = evt; assert(request.body); await respondWith(new Response(request.body)); })(); const ts = new TransformStream(); const writable = ts.writable.getWriter(); const resp = await fetch(`http://127.0.0.1:${listenPort}/`, { method: "POST", body: ts.readable, }); assert(resp.body); const reader = resp.body.getReader(); await writable.write(new Uint8Array([1])); const chunk1 = await reader.read(); assert(!chunk1.done); assertEquals(chunk1.value, new Uint8Array([1])); await writable.write(new Uint8Array([2])); const chunk2 = await reader.read(); assert(!chunk2.done); assertEquals(chunk2.value, new Uint8Array([2])); await writable.close(); const chunk3 = await reader.read(); assert(chunk3.done); await promise; httpConn!.close(); listener.close(); }, ); Deno.test({ permissions: { net: true } }, async function httpServerClose() { const listener = Deno.listen({ port: listenPort }); const client = await Deno.connect({ port: listenPort }); const httpConn = Deno.serveHttp(await listener.accept()); client.close(); const evt = await httpConn.nextRequest(); assertEquals(evt, null); // Note httpConn is automatically closed when "done" is reached. listener.close(); }); Deno.test( { permissions: { net: true } }, async function httpServerInvalidMethod() { const listener = Deno.listen({ port: listenPort }); const client = await Deno.connect({ port: listenPort }); const httpConn = Deno.serveHttp(await listener.accept()); await client.write(new Uint8Array([1, 2, 3])); await assertRejects( async () => { await httpConn.nextRequest(); }, Deno.errors.Http, "invalid HTTP method parsed", ); // Note httpConn is automatically closed when it errors. client.close(); listener.close(); }, ); Deno.test( { permissions: { read: true, net: true } }, async function httpServerWithTls() { const hostname = "localhost"; const port = listenPort; const promise = (async () => { const listener = Deno.listenTls({ hostname, port, cert: Deno.readTextFileSync("tests/testdata/tls/localhost.crt"), key: Deno.readTextFileSync("tests/testdata/tls/localhost.key"), }); const conn = await listener.accept(); const httpConn = Deno.serveHttp(conn); const evt = await httpConn.nextRequest(); assert(evt); const { respondWith } = evt; await respondWith(new Response("Hello World")); // TODO(ry) If we don't call httpConn.nextRequest() here we get "error sending // request for url (https://localhost:${listenPort}/): connection closed before // message completed". assertEquals(await httpConn.nextRequest(), null); listener.close(); })(); const caCert = Deno.readTextFileSync("tests/testdata/tls/RootCA.pem"); const client = Deno.createHttpClient({ caCerts: [caCert] }); const resp = await fetch(`https://${hostname}:${port}/`, { headers: { "connection": "close" }, client, }); client.close(); const respBody = await resp.text(); assertEquals("Hello World", respBody); await promise; }, ); Deno.test( { permissions: { net: true } }, async function httpServerRegressionHang() { let httpConn: Deno.HttpConn; const listener = Deno.listen({ port: listenPort }); const promise = (async () => { const conn = await listener.accept(); httpConn = Deno.serveHttp(conn); const event = await httpConn.nextRequest(); assert(event); const { request, respondWith } = event; const reqBody = await request.text(); assertEquals("request", reqBody); await respondWith(new Response("response")); })(); const resp = await fetch(`http://127.0.0.1:${listenPort}/`, { method: "POST", body: "request", }); const respBody = await resp.text(); assertEquals("response", respBody); await promise; httpConn!.close(); listener.close(); }, ); Deno.test( { permissions: { net: true } }, async function httpServerCancelBodyOnResponseFailure() { const promise = (async () => { const listener = Deno.listen({ port: listenPort }); const conn = await listener.accept(); const httpConn = Deno.serveHttp(conn); const event = await httpConn.nextRequest(); assert(event); const { respondWith } = event; let cancelReason: string; await assertRejects( async () => { let interval = 0; await respondWith( new Response( new ReadableStream({ start(controller) { interval = setInterval(() => { const message = `data: ${Date.now()}\n\n`; controller.enqueue(new TextEncoder().encode(message)); }, 200); }, cancel(reason) { cancelReason = reason; clearInterval(interval); }, }), ), ); }, Deno.errors.Http, cancelReason!, ); assert(cancelReason!); httpConn!.close(); listener.close(); })(); const resp = await fetch(`http://127.0.0.1:${listenPort}/`); await resp.body!.cancel(); await promise; }, ); Deno.test( { permissions: { net: true } }, async function httpServerNextRequestErrorExposedInResponse() { const promise = (async () => { const listener = Deno.listen({ port: listenPort }); const conn = await listener.accept(); const httpConn = Deno.serveHttp(conn); const event = await httpConn.nextRequest(); assert(event); // Start polling for the next request before awaiting response. const nextRequestPromise = httpConn.nextRequest(); const { respondWith } = event; await assertRejects( async () => { let interval = 0; await respondWith( new Response( new ReadableStream({ start(controller) { interval = setInterval(() => { const message = `data: ${Date.now()}\n\n`; controller.enqueue(new TextEncoder().encode(message)); }, 200); }, cancel() { clearInterval(interval); }, }), ), ); }, Deno.errors.Http, "connection closed", ); // The error from `op_http_accept` reroutes to `respondWith()`. assertEquals(await nextRequestPromise, null); listener.close(); })(); const resp = await fetch(`http://127.0.0.1:${listenPort}/`); await resp.body!.cancel(); await promise; }, ); Deno.test( { permissions: { net: true } }, async function httpServerEmptyBlobResponse() { let httpConn: Deno.HttpConn; const listener = Deno.listen({ port: listenPort }); const promise = (async () => { const conn = await listener.accept(); httpConn = Deno.serveHttp(conn); const event = await httpConn.nextRequest(); assert(event); const { respondWith } = event; await respondWith(new Response(new Blob([]))); })(); const resp = await fetch(`http://127.0.0.1:${listenPort}/`); const respBody = await resp.text(); assertEquals("", respBody); await promise; httpConn!.close(); listener.close(); }, ); Deno.test( { permissions: { net: true } }, async function httpServerNextRequestResolvesOnClose() { const httpConnList: Deno.HttpConn[] = []; async function serve(l: Deno.Listener) { for await (const conn of l) { (async () => { const c = Deno.serveHttp(conn); httpConnList.push(c); for await (const { respondWith } of c) { respondWith(new Response("hello")); } })(); } } const l = Deno.listen({ port: listenPort }); serve(l); await delay(300); const res = await fetch(`http://localhost:${listenPort}/`); const _text = await res.text(); // Close connection and listener. httpConnList.forEach((conn) => conn.close()); l.close(); await delay(300); }, ); Deno.test( { permissions: { net: true } }, // Issue: https://github.com/denoland/deno/issues/10870 async function httpServerHang() { // Quick and dirty way to make a readable stream from a string. Alternatively, // `readableStreamFromReader(file)` could be used. function stream(s: string): ReadableStream { return new Response(s).body!; } const httpConns: Deno.HttpConn[] = []; const promise = (async () => { let count = 0; const listener = Deno.listen({ port: listenPort }); for await (const conn of listener) { (async () => { const httpConn = Deno.serveHttp(conn); httpConns.push(httpConn); for await (const { respondWith } of httpConn) { respondWith(new Response(stream("hello"))); count++; if (count >= 2) { listener.close(); } } })(); } })(); const clientConn = await Deno.connect({ port: listenPort }); const r1 = await writeRequestAndReadResponse(clientConn); assertEquals(r1, "hello"); const r2 = await writeRequestAndReadResponse(clientConn); assertEquals(r2, "hello"); clientConn.close(); await promise; for (const conn of httpConns) { conn.close(); } }, ); Deno.test( { permissions: { net: true } }, // Issue: https://github.com/denoland/deno/issues/10930 async function httpServerStreamingResponse() { // This test enqueues a single chunk for readable // stream and waits for client to read that chunk and signal // it before enqueueing subsequent chunk. Issue linked above // presented a situation where enqueued chunks were not // written to the HTTP connection until the next chunk was enqueued. let counter = 0; const deferreds = [ Promise.withResolvers(), Promise.withResolvers(), Promise.withResolvers(), ]; async function writeRequest(conn: Deno.Conn) { const encoder = new TextEncoder(); const decoder = new TextDecoder(); const w = new BufWriter(conn); const r = new BufReader(conn); const body = `GET / HTTP/1.1\r\nHost: 127.0.0.1:${listenPort}\r\n\r\n`; const writeResult = await w.write(encoder.encode(body)); assertEquals(body.length, writeResult); await w.flush(); const tpr = new TextProtoReader(r); const statusLine = await tpr.readLine(); assert(statusLine !== null); const headers = await tpr.readMimeHeader(); assert(headers !== null); const chunkedReader = chunkedBodyReader(headers, r); const buf = new Uint8Array(5); const dest = new Buffer(); let result: number | null; while ((result = await chunkedReader.read(buf)) !== null) { const len = Math.min(buf.byteLength, result); await dest.write(buf.subarray(0, len)); // Resolve a deferred - this will make response stream to // enqueue next chunk. deferreds[counter - 1].resolve(); } return decoder.decode(dest.bytes()); } function periodicStream() { return new ReadableStream({ start(controller) { controller.enqueue(`${counter}\n`); counter++; }, async pull(controller) { if (counter >= 3) { return controller.close(); } await deferreds[counter - 1].promise; controller.enqueue(`${counter}\n`); counter++; }, }).pipeThrough(new TextEncoderStream()); } let httpConn: Deno.HttpConn; const listener = Deno.listen({ port: listenPort }); const finished = (async () => { const conn = await listener.accept(); httpConn = Deno.serveHttp(conn); const requestEvent = await httpConn.nextRequest(); const { respondWith } = requestEvent!; await respondWith(new Response(periodicStream())); })(); // start a client const clientConn = await Deno.connect({ port: listenPort }); const r1 = await writeRequest(clientConn); assertEquals(r1, "0\n1\n2\n"); await finished; clientConn.close(); httpConn!.close(); listener.close(); }, ); Deno.test( { permissions: { net: true } }, async function httpRequestLatin1Headers() { let httpConn: Deno.HttpConn; const promise = (async () => { const listener = Deno.listen({ port: listenPort }); const conn = await listener.accept(); listener.close(); httpConn = Deno.serveHttp(conn); const reqEvent = await httpConn.nextRequest(); assert(reqEvent); const { request, respondWith } = reqEvent; assertEquals(request.headers.get("X-Header-Test"), "á"); await respondWith( new Response("", { headers: { "X-Header-Test": "Æ" } }), ); })(); const clientConn = await Deno.connect({ port: listenPort }); const requestText = `GET / HTTP/1.1\r\nHost: 127.0.0.1:${listenPort}\r\nX-Header-Test: á\r\n\r\n`; const requestBytes = new Uint8Array(requestText.length); for (let i = 0; i < requestText.length; i++) { requestBytes[i] = requestText.charCodeAt(i); } let written = 0; while (written < requestBytes.byteLength) { written += await clientConn.write(requestBytes.slice(written)); } let responseText = ""; const buf = new Uint8Array(1024); let read; while ((read = await clientConn.read(buf)) !== null) { httpConn!.close(); for (let i = 0; i < read; i++) { responseText += String.fromCharCode(buf[i]); } } clientConn.close(); assert(/\r\n[Xx]-[Hh]eader-[Tt]est: Æ\r\n/.test(responseText)); await promise; }, ); Deno.test( { permissions: { net: true } }, async function httpServerRequestWithoutPath() { let httpConn: Deno.HttpConn; const listener = Deno.listen({ port: listenPort }); const promise = (async () => { const conn = await listener.accept(); listener.close(); httpConn = Deno.serveHttp(conn); const reqEvent = await httpConn.nextRequest(); assert(reqEvent); const { request, respondWith } = reqEvent; assertEquals( new URL(request.url).href, `http://127.0.0.1:${listenPort}/`, ); assertEquals(await request.text(), ""); await respondWith(new Response()); })(); const clientConn = await Deno.connect({ port: listenPort }); async function writeRequest(conn: Deno.Conn) { const encoder = new TextEncoder(); const w = new BufWriter(conn); const r = new BufReader(conn); const body = `CONNECT 127.0.0.1:${listenPort} HTTP/1.1\r\nHost: 127.0.0.1:${listenPort}\r\n\r\n`; const writeResult = await w.write(encoder.encode(body)); assertEquals(body.length, writeResult); await w.flush(); const tpr = new TextProtoReader(r); const statusLine = await tpr.readLine(); assert(statusLine !== null); const m = statusLine.match(/^(.+?) (.+?) (.+?)$/); assert(m !== null, "must be matched"); const [_, _proto, status, _ok] = m; assertEquals(status, "200"); const headers = await tpr.readMimeHeader(); assert(headers !== null); } await writeRequest(clientConn); clientConn.close(); await promise; httpConn!.close(); }, ); Deno.test({ permissions: { net: true } }, async function httpServerWebSocket() { const promise = (async () => { const listener = Deno.listen({ port: listenPort }); const conn = await listener.accept(); listener.close(); const httpConn = Deno.serveHttp(conn); const reqEvent = await httpConn.nextRequest(); assert(reqEvent); const { request, respondWith } = reqEvent; const { response, socket, } = Deno.upgradeWebSocket(request); socket.onerror = () => fail(); socket.onmessage = (m) => { socket.send(m.data); socket.close(1001); }; const close = new Promise((resolve) => { socket.onclose = () => resolve(); }); await respondWith(response); await close; })(); const def = Promise.withResolvers(); const ws = new WebSocket(`ws://localhost:${listenPort}`); ws.onmessage = (m) => assertEquals(m.data, "foo"); ws.onerror = () => fail(); ws.onclose = () => def.resolve(); ws.onopen = () => ws.send("foo"); await def.promise; await promise; }); Deno.test(function httpUpgradeWebSocket() { const request = new Request("https://deno.land/", { headers: { connection: "Upgrade", upgrade: "websocket", "sec-websocket-key": "dGhlIHNhbXBsZSBub25jZQ==", }, }); const { response } = Deno.upgradeWebSocket(request); assertEquals(response.status, 101); assertEquals(response.headers.get("connection"), "Upgrade"); assertEquals(response.headers.get("upgrade"), "websocket"); assertEquals( response.headers.get("sec-websocket-accept"), "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=", ); }); Deno.test(function httpUpgradeWebSocketMultipleConnectionOptions() { const request = new Request("https://deno.land/", { headers: { connection: "keep-alive, upgrade", upgrade: "websocket", "sec-websocket-key": "dGhlIHNhbXBsZSBub25jZQ==", }, }); const { response } = Deno.upgradeWebSocket(request); assertEquals(response.status, 101); }); Deno.test(function httpUpgradeWebSocketMultipleUpgradeOptions() { const request = new Request("https://deno.land/", { headers: { connection: "upgrade", upgrade: "websocket, foo", "sec-websocket-key": "dGhlIHNhbXBsZSBub25jZQ==", }, }); const { response } = Deno.upgradeWebSocket(request); assertEquals(response.status, 101); }); Deno.test(function httpUpgradeWebSocketCaseInsensitiveUpgradeHeader() { const request = new Request("https://deno.land/", { headers: { connection: "upgrade", upgrade: "Websocket", "sec-websocket-key": "dGhlIHNhbXBsZSBub25jZQ==", }, }); const { response } = Deno.upgradeWebSocket(request); assertEquals(response.status, 101); }); Deno.test(function httpUpgradeWebSocketInvalidUpgradeHeader() { assertThrows( () => { const request = new Request("https://deno.land/", { headers: { connection: "upgrade", upgrade: "invalid", "sec-websocket-key": "dGhlIHNhbXBsZSBub25jZQ==", }, }); Deno.upgradeWebSocket(request); }, TypeError, "Invalid Header: 'upgrade' header must contain 'websocket'", ); }); Deno.test(function httpUpgradeWebSocketWithoutUpgradeHeader() { assertThrows( () => { const request = new Request("https://deno.land/", { headers: { connection: "upgrade", "sec-websocket-key": "dGhlIHNhbXBsZSBub25jZQ==", }, }); Deno.upgradeWebSocket(request); }, TypeError, "Invalid Header: 'upgrade' header must contain 'websocket'", ); }); Deno.test( { permissions: { net: true } }, async function httpCookieConcatenation() { let httpConn: Deno.HttpConn; const promise = (async () => { const listener = Deno.listen({ port: listenPort }); const conn = await listener.accept(); listener.close(); httpConn = Deno.serveHttp(conn); const reqEvent = await httpConn.nextRequest(); assert(reqEvent); const { request, respondWith } = reqEvent; assertEquals( new URL(request.url).href, `http://127.0.0.1:${listenPort}/`, ); assertEquals(await request.text(), ""); assertEquals(request.headers.get("cookie"), "foo=bar; bar=foo"); await respondWith(new Response("ok")); })(); const resp = await fetch(`http://127.0.0.1:${listenPort}/`, { headers: [ ["connection", "close"], ["cookie", "foo=bar"], ["cookie", "bar=foo"], ], }); const text = await resp.text(); assertEquals(text, "ok"); await promise; httpConn!.close(); }, ); // https://github.com/denoland/deno/issues/11651 Deno.test({ permissions: { net: true } }, async function httpServerPanic() { const listener = Deno.listen({ port: listenPort }); const client = await Deno.connect({ port: listenPort }); const conn = await listener.accept(); const httpConn = Deno.serveHttp(conn); // This message is incomplete on purpose, we'll forcefully close client connection // after it's flushed to cause connection to error out on the server side. const encoder = new TextEncoder(); await client.write(encoder.encode("GET / HTTP/1.1")); httpConn.nextRequest(); await client.write(encoder.encode("\r\n\r\n")); httpConn!.close(); client.close(); listener.close(); }); Deno.test( { permissions: { net: true, write: true, read: true } }, async function httpServerCorrectSizeResponse() { const tmpFile = await Deno.makeTempFile(); using file = await Deno.open(tmpFile, { write: true, read: true }); await file.write(new Uint8Array(70 * 1024).fill(1)); // 70kb sent in 64kb + 6kb chunks let httpConn: Deno.HttpConn; const listener = Deno.listen({ port: listenPort }); const promise = (async () => { const conn = await listener.accept(); httpConn = Deno.serveHttp(conn); const ev = await httpConn.nextRequest(); const { respondWith } = ev!; const f = await Deno.open(tmpFile, { read: true }); await respondWith(new Response(f.readable, { status: 200 })); })(); const resp = await fetch(`http://127.0.0.1:${listenPort}/`); const body = await resp.bytes(); assertEquals(body.byteLength, 70 * 1024); await promise; httpConn!.close(); listener.close(); }, ); Deno.test( { permissions: { net: true, write: true, read: true } }, async function httpServerClosedStream() { const listener = Deno.listen({ port: listenPort }); const client = await Deno.connect({ port: listenPort }); await client.write(new TextEncoder().encode( `GET / HTTP/1.0\r\n\r\n`, )); const conn = await listener.accept(); const httpConn = Deno.serveHttp(conn); const ev = await httpConn.nextRequest(); const { respondWith } = ev!; const tmpFile = await Deno.makeTempFile(); const file = await Deno.open(tmpFile, { write: true, read: true }); await file.write(new TextEncoder().encode("hello")); const reader = await file.readable.getReader(); while (true) { const { done, value } = await reader.read(); if (done) break; assert(value); } let didThrow = false; try { await respondWith(new Response(file.readable)); } catch { // pass didThrow = true; } assert(didThrow); httpConn!.close(); listener.close(); client.close(); }, ); // https://github.com/denoland/deno/issues/11595 Deno.test( { permissions: { net: true } }, async function httpServerIncompleteMessage() { const listener = Deno.listen({ port: listenPort }); const client = await Deno.connect({ port: listenPort }); await client.write(new TextEncoder().encode( `GET / HTTP/1.0\r\n\r\n`, )); const conn = await listener.accept(); const httpConn = Deno.serveHttp(conn); const ev = await httpConn.nextRequest(); const { respondWith } = ev!; const errors: Error[] = []; const readable = new ReadableStream({ async pull(controller) { client.close(); await delay(1000); controller.enqueue(new TextEncoder().encode( "written to the writable side of a TransformStream", )); controller.close(); }, cancel(error) { errors.push(error); }, }); const res = new Response(readable); await respondWith(res).catch((error: Error) => errors.push(error)); httpConn!.close(); listener.close(); assert(errors.length >= 1); for (const error of errors) { assertEquals(error.name, "Http"); assert(error.message.includes("connection")); } }, ); // https://github.com/denoland/deno/issues/11743 Deno.test( { permissions: { net: true } }, async function httpServerDoesntLeakResources() { const listener = Deno.listen({ port: listenPort }); const [conn, clientConn] = await Promise.all([ listener.accept(), Deno.connect({ port: listenPort }), ]); const httpConn = Deno.serveHttp(conn); await Promise.all([ httpConn.nextRequest(), clientConn.write(new TextEncoder().encode( `GET / HTTP/1.1\r\nHost: 127.0.0.1:${listenPort}\r\n\r\n`, )), ]); httpConn!.close(); listener.close(); clientConn.close(); }, ); // https://github.com/denoland/deno/issues/11926 // verify that the only new resource is "httpConnection", to make // sure "request" resource is closed even if its body was not read // by server handler Deno.test( { permissions: { net: true } }, async function httpServerDoesntLeakResources2() { let listener: Deno.Listener; let httpConn: Deno.HttpConn; const promise = (async () => { listener = Deno.listen({ port: listenPort }); for await (const conn of listener) { httpConn = Deno.serveHttp(conn); for await (const { request, respondWith } of httpConn) { assertEquals( new URL(request.url).href, `http://127.0.0.1:${listenPort}/`, ); // not reading request body on purpose respondWith(new Response("ok")); } } })(); const response = await fetch(`http://127.0.0.1:${listenPort}`, { method: "POST", body: "hello world", }); await response.text(); listener!.close(); httpConn!.close(); await promise; }, ); // https://github.com/denoland/deno/pull/12216 Deno.test( { permissions: { net: true } }, async function droppedConnSenderNoPanic() { async function server() { const listener = Deno.listen({ port: listenPort }); const conn = await listener.accept(); const http = Deno.serveHttp(conn); const evt = await http.nextRequest(); http.close(); try { await evt!.respondWith(new Response("boom")); } catch { // Ignore error. } listener.close(); } async function client() { try { const resp = await fetch(`http://127.0.0.1:${listenPort}/`); await resp.body?.cancel(); } catch { // Ignore error } } await Promise.all([server(), client()]); }, ); // https://github.com/denoland/deno/issues/12193 Deno.test( { permissions: { net: true } }, async function httpConnConcurrentNextRequestCalls() { const hostname = "localhost"; const port = listenPort; let httpConn: Deno.HttpConn; const listener = Deno.listen({ hostname, port }); async function server() { const tcpConn = await listener.accept(); httpConn = Deno.serveHttp(tcpConn); const promises = new Array(10).fill(null).map(async (_, i) => { const event = await httpConn.nextRequest(); assert(event); const { pathname } = new URL(event.request.url); assertStrictEquals(pathname, `/${i}`); const response = new Response(`Response #${i}`); await event.respondWith(response); }); await Promise.all(promises); } async function client() { for (let i = 0; i < 10; i++) { const response = await fetch(`http://${hostname}:${port}/${i}`); const body = await response.text(); assertStrictEquals(body, `Response #${i}`); } } await Promise.all([server(), delay(100).then(client)]); httpConn!.close(); listener.close(); }, ); // https://github.com/denoland/deno/pull/12704 // https://github.com/denoland/deno/pull/12732 Deno.test( { permissions: { net: true } }, async function httpConnAutoCloseDelayedOnUpgrade() { const hostname = "localhost"; const port = listenPort; async function server() { const listener = Deno.listen({ hostname, port }); const tcpConn = await listener.accept(); const httpConn = Deno.serveHttp(tcpConn); const event1 = await httpConn.nextRequest() as Deno.RequestEvent; const event2Promise = httpConn.nextRequest(); const { socket, response } = Deno.upgradeWebSocket(event1.request); socket.onmessage = (event) => socket.send(event.data); const socketClosed = new Promise((resolve) => { socket.onclose = () => resolve(); }); event1.respondWith(response); const event2 = await event2Promise; assertStrictEquals(event2, null); listener.close(); await socketClosed; } async function client() { const socket = new WebSocket(`ws://${hostname}:${port}/`); socket.onopen = () => socket.send("bla bla"); const closed = new Promise((resolve) => { socket.onclose = () => resolve(); }); const { data } = await new Promise>((res) => socket.onmessage = res ); assertStrictEquals(data, "bla bla"); socket.close(); await closed; } await Promise.all([server(), client()]); }, ); // https://github.com/denoland/deno/issues/12741 // https://github.com/denoland/deno/pull/12746 // https://github.com/denoland/deno/pull/12798 Deno.test( { permissions: { net: true, run: true } }, async function httpServerDeleteRequestHasBody() { const hostname = "localhost"; const port = listenPort; let httpConn: Deno.HttpConn; const listener = Deno.listen({ hostname, port }); async function server() { const tcpConn = await listener.accept(); httpConn = Deno.serveHttp(tcpConn); const event = await httpConn.nextRequest() as Deno.RequestEvent; assert(event.request.body); const response = new Response(); await event.respondWith(response); } async function client() { const url = `http://${hostname}:${port}/`; const args = ["-X", "DELETE", url]; const { success } = await new Deno.Command("curl", { args, stdout: "null", stderr: "null", }).output(); assert(success); } await Promise.all([server(), client()]); httpConn!.close(); listener.close(); }, ); Deno.test( { permissions: { net: true } }, async function httpServerRespondNonAsciiUint8Array() { let httpConn: Deno.HttpConn; const listener = Deno.listen({ port: listenPort }); const promise = (async () => { const conn = await listener.accept(); listener.close(); httpConn = Deno.serveHttp(conn); const e = await httpConn.nextRequest(); assert(e); const { request, respondWith } = e; assertEquals(request.body, null); await respondWith( new Response(new Uint8Array([128]), {}), ); })(); const resp = await fetch(`http://localhost:${listenPort}/`); assertEquals(resp.status, 200); const body = await resp.bytes(); assertEquals(body, new Uint8Array([128])); await promise; httpConn!.close(); }, ); function tmpUnixSocketPath(): string { const folder = Deno.makeTempDirSync(); return join(folder, "socket"); } // https://github.com/denoland/deno/pull/13628 Deno.test( { ignore: Deno.build.os === "windows", permissions: { read: true, write: true }, }, async function httpServerOnUnixSocket() { const filePath = tmpUnixSocketPath(); let httpConn: Deno.HttpConn; const promise = (async () => { const listener = Deno.listen({ path: filePath, transport: "unix" }); const conn = await listener.accept(); listener.close(); httpConn = Deno.serveHttp(conn); const reqEvent = await httpConn.nextRequest(); assert(reqEvent); const { request, respondWith } = reqEvent; const url = new URL(request.url); assertEquals(url.protocol, "http+unix:"); assertEquals(decodeURIComponent(url.host), filePath); assertEquals(url.pathname, "/path/name"); await respondWith(new Response("", { headers: {} })); })(); // fetch() does not supports unix domain sockets yet https://github.com/denoland/deno/issues/8821 const conn = await Deno.connect({ path: filePath, transport: "unix" }); const encoder = new TextEncoder(); // The Host header must be present and empty if it is not a Internet host name (RFC2616, Section 14.23) const body = `GET /path/name HTTP/1.1\r\nHost:\r\n\r\n`; const writeResult = await conn.write(encoder.encode(body)); assertEquals(body.length, writeResult); const resp = new Uint8Array(200); const readResult = await conn.read(resp); assertEquals(readResult, 138); conn.close(); await promise; httpConn!.close(); }, ); /* Automatic Body Compression */ const decoder = new TextDecoder(); Deno.test({ name: "http server compresses body - check headers", permissions: { net: true, run: true }, async fn() { const hostname = "localhost"; const port = listenPort; const listener = Deno.listen({ hostname, port }); const data = { hello: "deno", now: "with", compressed: "body" }; let httpConn: Deno.HttpConn; async function server() { const tcpConn = await listener.accept(); httpConn = Deno.serveHttp(tcpConn); const e = await httpConn.nextRequest(); assert(e); const { request, respondWith } = e; assertEquals(request.headers.get("Accept-Encoding"), "gzip, deflate, br"); const response = new Response(JSON.stringify(data), { headers: { "content-type": "application/json" }, }); await respondWith(response); listener.close(); } async function client() { const url = `http://${hostname}:${port}/`; const args = [ "-i", "--request", "GET", "--url", url, "--header", "Accept-Encoding: gzip, deflate, br", ]; const { success, stdout } = await new Deno.Command("curl", { args, stderr: "null", stdout: "piped", }).output(); assert(success); const output = decoder.decode(stdout); assert(output.includes("vary: Accept-Encoding\r\n")); assert(output.includes("content-encoding: gzip\r\n")); } await Promise.all([server(), client()]); httpConn!.close(); }, }); Deno.test({ name: "http server compresses body - check body", permissions: { net: true, run: true }, async fn() { const hostname = "localhost"; const port = listenPort; const listener = Deno.listen({ hostname, port }); const data = { hello: "deno", now: "with", compressed: "body" }; let httpConn: Deno.HttpConn; async function server() { const tcpConn = await listener.accept(); httpConn = Deno.serveHttp(tcpConn); const e = await httpConn.nextRequest(); assert(e); const { request, respondWith } = e; assertEquals(request.headers.get("Accept-Encoding"), "gzip, deflate, br"); const response = new Response(JSON.stringify(data), { headers: { "content-type": "application/json" }, }); await respondWith(response); listener.close(); } async function client() { const url = `http://${hostname}:${port}/`; const args = [ "--request", "GET", "--url", url, "--header", "Accept-Encoding: gzip, deflate, br", ]; const proc = new Deno.Command("curl", { args, stderr: "null", stdout: "piped", }).spawn(); const status = await proc.status; assert(status.success); const stdout = proc.stdout .pipeThrough(new DecompressionStream("gzip")) .pipeThrough(new TextDecoderStream()); let body = ""; for await (const chunk of stdout) { body += chunk; } assertEquals(JSON.parse(body), data); } await Promise.all([server(), client()]); httpConn!.close(); }, }); Deno.test({ name: "http server doesn't compress small body", permissions: { net: true, run: true }, async fn() { const hostname = "localhost"; const port = listenPort; let httpConn: Deno.HttpConn; async function server() { const listener = Deno.listen({ hostname, port }); const tcpConn = await listener.accept(); httpConn = Deno.serveHttp(tcpConn); const e = await httpConn.nextRequest(); assert(e); const { request, respondWith } = e; assertEquals(request.headers.get("Accept-Encoding"), "gzip, deflate, br"); const response = new Response( JSON.stringify({ hello: "deno" }), { headers: { "content-type": "application/json" }, }, ); await respondWith(response); listener.close(); } async function client() { const url = `http://${hostname}:${port}/`; const args = [ "-i", "--request", "GET", "--url", url, "--header", "Accept-Encoding: gzip, deflate, br", ]; const { success, stdout } = await new Deno.Command("curl", { args, stderr: "null", stdout: "piped", }).output(); assert(success); const output = decoder.decode(stdout).toLocaleLowerCase(); assert(output.includes("vary: accept-encoding\r\n")); assert(!output.includes("content-encoding: ")); } await Promise.all([server(), client()]); httpConn!.close(); }, }); Deno.test({ name: "http server respects accept-encoding weights", permissions: { net: true, run: true }, async fn() { const hostname = "localhost"; const port = listenPort; let httpConn: Deno.HttpConn; async function server() { const listener = Deno.listen({ hostname, port }); const tcpConn = await listener.accept(); httpConn = Deno.serveHttp(tcpConn); const e = await httpConn.nextRequest(); assert(e); const { request, respondWith } = e; assertEquals( request.headers.get("Accept-Encoding"), "gzip;q=0.8, br;q=1.0, *;q=0.1", ); const response = new Response( JSON.stringify({ hello: "deno", now: "with", compressed: "body" }), { headers: { "content-type": "application/json" }, }, ); await respondWith(response); listener.close(); } async function client() { const url = `http://${hostname}:${port}/`; const args = [ "-i", "--request", "GET", "--url", url, "--header", "Accept-Encoding: gzip;q=0.8, br;q=1.0, *;q=0.1", ]; const { success, stdout } = await new Deno.Command("curl", { args, stderr: "null", stdout: "piped", }).output(); assert(success); const output = decoder.decode(stdout); assert(output.includes("vary: Accept-Encoding\r\n")); assert(output.includes("content-encoding: br\r\n")); } await Promise.all([server(), client()]); httpConn!.close(); }, }); Deno.test({ name: "http server augments vary header", permissions: { net: true, run: true }, async fn() { const hostname = "localhost"; const port = listenPort; let httpConn: Deno.HttpConn; async function server() { const listener = Deno.listen({ hostname, port }); const tcpConn = await listener.accept(); httpConn = Deno.serveHttp(tcpConn); const e = await httpConn.nextRequest(); assert(e); const { request, respondWith } = e; assertEquals(request.headers.get("Accept-Encoding"), "gzip, deflate, br"); const response = new Response( JSON.stringify({ hello: "deno", now: "with", compressed: "body" }), { headers: { "content-type": "application/json", vary: "Accept" }, }, ); await respondWith(response); listener.close(); } async function client() { const url = `http://${hostname}:${port}/`; const args = [ "-i", "--request", "GET", "--url", url, "--header", "Accept-Encoding: gzip, deflate, br", ]; const { success, stdout } = await new Deno.Command("curl", { args, stderr: "null", stdout: "piped", }).output(); assert(success); const output = decoder.decode(stdout); assert(output.includes("vary: Accept-Encoding, Accept\r\n")); assert(output.includes("content-encoding: gzip\r\n")); } await Promise.all([server(), client()]); httpConn!.close(); }, }); Deno.test({ name: "http server weakens etag header", permissions: { net: true, run: true }, async fn() { const hostname = "localhost"; const port = listenPort; let httpConn: Deno.HttpConn; async function server() { const listener = Deno.listen({ hostname, port }); const tcpConn = await listener.accept(); httpConn = Deno.serveHttp(tcpConn); const e = await httpConn.nextRequest(); assert(e); const { request, respondWith } = e; assertEquals(request.headers.get("Accept-Encoding"), "gzip, deflate, br"); const response = new Response( JSON.stringify({ hello: "deno", now: "with", compressed: "body" }), { headers: { "content-type": "application/json", etag: "33a64df551425fcc55e4d42a148795d9f25f89d4", }, }, ); await respondWith(response); listener.close(); } async function client() { const url = `http://${hostname}:${port}/`; const args = [ "curl", "-i", "--request", "GET", "--url", url, "--header", "Accept-Encoding: gzip, deflate, br", ]; const { success, stdout } = await new Deno.Command("curl", { args, stderr: "null", stdout: "piped", }).output(); assert(success); const output = decoder.decode(stdout); assert(output.includes("vary: Accept-Encoding\r\n")); assert( output.includes("etag: W/33a64df551425fcc55e4d42a148795d9f25f89d4\r\n"), ); assert(output.includes("content-encoding: gzip\r\n")); } await Promise.all([server(), client()]); httpConn!.close(); }, }); Deno.test({ name: "http server passes through weak etag header", permissions: { net: true, run: true }, async fn() { const hostname = "localhost"; const port = listenPort; let httpConn: Deno.HttpConn; async function server() { const listener = Deno.listen({ hostname, port }); const tcpConn = await listener.accept(); httpConn = Deno.serveHttp(tcpConn); const e = await httpConn.nextRequest(); assert(e); const { request, respondWith } = e; assertEquals(request.headers.get("Accept-Encoding"), "gzip, deflate, br"); const response = new Response( JSON.stringify({ hello: "deno", now: "with", compressed: "body" }), { headers: { "content-type": "application/json", etag: "W/33a64df551425fcc55e4d42a148795d9f25f89d4", }, }, ); await respondWith(response); listener.close(); } async function client() { const url = `http://${hostname}:${port}/`; const args = [ "-i", "--request", "GET", "--url", url, "--header", "Accept-Encoding: gzip, deflate, br", ]; const { success, stdout } = await new Deno.Command("curl", { args, stderr: "null", stdout: "piped", }).output(); assert(success); const output = decoder.decode(stdout); assert(output.includes("vary: Accept-Encoding\r\n")); assert( output.includes("etag: W/33a64df551425fcc55e4d42a148795d9f25f89d4\r\n"), ); assert(output.includes("content-encoding: gzip\r\n")); } await Promise.all([server(), client()]); httpConn!.close(); }, }); Deno.test({ name: "http server doesn't compress body when no-transform is set", permissions: { net: true, run: true }, async fn() { const hostname = "localhost"; const port = listenPort; let httpConn: Deno.HttpConn; async function server() { const listener = Deno.listen({ hostname, port }); const tcpConn = await listener.accept(); httpConn = Deno.serveHttp(tcpConn); const e = await httpConn.nextRequest(); assert(e); const { request, respondWith } = e; assertEquals(request.headers.get("Accept-Encoding"), "gzip, deflate, br"); const response = new Response( JSON.stringify({ hello: "deno", now: "with", compressed: "body" }), { headers: { "content-type": "application/json", "cache-control": "no-transform", }, }, ); await respondWith(response); listener.close(); } async function client() { const url = `http://${hostname}:${port}/`; const args = [ "-i", "--request", "GET", "--url", url, "--header", "Accept-Encoding: gzip, deflate, br", ]; const { success, stdout } = await new Deno.Command("curl", { args, stderr: "null", stdout: "piped", }).output(); assert(success); const output = decoder.decode(stdout); assert(output.includes("vary: Accept-Encoding\r\n")); assert(!output.includes("content-encoding: ")); } await Promise.all([server(), client()]); httpConn!.close(); }, }); Deno.test({ name: "http server doesn't compress body when content-range is set", permissions: { net: true, run: true }, async fn() { const hostname = "localhost"; const port = listenPort; let httpConn: Deno.HttpConn; async function server() { const listener = Deno.listen({ hostname, port }); const tcpConn = await listener.accept(); httpConn = Deno.serveHttp(tcpConn); const e = await httpConn.nextRequest(); assert(e); const { request, respondWith } = e; assertEquals(request.headers.get("Accept-Encoding"), "gzip, deflate, br"); const response = new Response( JSON.stringify({ hello: "deno", now: "with", compressed: "body" }), { headers: { "content-type": "application/json", "content-range": "bytes 200-100/67589", }, }, ); await respondWith(response); listener.close(); } async function client() { const url = `http://${hostname}:${port}/`; const args = [ "-i", "--request", "GET", "--url", url, "--header", "Accept-Encoding: gzip, deflate, br", ]; const { success, stdout } = await new Deno.Command("curl", { args, stderr: "null", stdout: "piped", }).output(); assert(success); const output = decoder.decode(stdout); assert(output.includes("vary: Accept-Encoding\r\n")); assert(!output.includes("content-encoding: ")); } await Promise.all([server(), client()]); httpConn!.close(); }, }); Deno.test({ name: "http server compresses streamed bodies - check headers", permissions: { net: true, run: true }, async fn() { const hostname = "localhost"; const port = listenPort; const encoder = new TextEncoder(); const listener = Deno.listen({ hostname, port }); const data = { hello: "deno", now: "with", compressed: "body" }; let httpConn: Deno.HttpConn; async function server() { const tcpConn = await listener.accept(); httpConn = Deno.serveHttp(tcpConn); const e = await httpConn.nextRequest(); assert(e); const { request, respondWith } = e; assertEquals(request.headers.get("Accept-Encoding"), "gzip, deflate, br"); const bodyInit = new ReadableStream({ start(controller) { controller.enqueue(encoder.encode(JSON.stringify(data))); controller.close(); }, }); const response = new Response( bodyInit, { headers: { "content-type": "application/json" } }, ); await respondWith(response); listener.close(); } async function client() { const url = `http://${hostname}:${port}/`; const args = [ "curl", "-i", "--request", "GET", "--url", url, "--header", "Accept-Encoding: gzip, deflate, br", ]; const { success, stdout } = await new Deno.Command("curl", { args, stderr: "null", stdout: "piped", }).output(); assert(success); const output = decoder.decode(stdout); assert(output.includes("vary: Accept-Encoding\r\n")); assert(output.includes("content-encoding: gzip\r\n")); } await Promise.all([server(), client()]); httpConn!.close(); }, }); Deno.test({ name: "http server compresses streamed bodies - check body", permissions: { net: true, run: true }, async fn() { const hostname = "localhost"; const port = listenPort; const encoder = new TextEncoder(); const listener = Deno.listen({ hostname, port }); const data = { hello: "deno", now: "with", compressed: "body" }; let httpConn: Deno.HttpConn; async function server() { const tcpConn = await listener.accept(); httpConn = Deno.serveHttp(tcpConn); const e = await httpConn.nextRequest(); assert(e); const { request, respondWith } = e; assertEquals(request.headers.get("Accept-Encoding"), "gzip, deflate, br"); const bodyInit = new ReadableStream({ start(controller) { controller.enqueue(encoder.encode(JSON.stringify(data))); controller.close(); }, }); const response = new Response( bodyInit, { headers: { "content-type": "application/json" } }, ); await respondWith(response); listener.close(); } async function client() { const url = `http://${hostname}:${port}/`; const args = [ "--request", "GET", "--url", url, "--header", "Accept-Encoding: gzip, deflate, br", ]; const proc = new Deno.Command("curl", { args, stderr: "null", stdout: "piped", }).spawn(); const status = await proc.status; assert(status.success); const stdout = proc.stdout .pipeThrough(new DecompressionStream("gzip")) .pipeThrough(new TextDecoderStream()); let body = ""; for await (const chunk of stdout) { body += chunk; } assertEquals(JSON.parse(body), data); } await Promise.all([server(), client()]); httpConn!.close(); }, }); Deno.test({ name: "http server updates content-length header if compression is applied", permissions: { net: true, run: true }, async fn() { const hostname = "localhost"; const port = listenPort; let contentLength: string; let httpConn: Deno.HttpConn; async function server() { const listener = Deno.listen({ hostname, port }); const tcpConn = await listener.accept(); httpConn = Deno.serveHttp(tcpConn); const e = await httpConn.nextRequest(); assert(e); const { request, respondWith } = e; assertEquals(request.headers.get("Accept-Encoding"), "gzip, deflate, br"); const body = JSON.stringify({ hello: "deno", now: "with", compressed: "body", }); contentLength = String(body.length); const response = new Response( body, { headers: { "content-type": "application/json", "content-length": contentLength, }, }, ); await respondWith(response); listener.close(); } async function client() { const url = `http://${hostname}:${port}/`; const args = [ "-i", "--request", "GET", "--url", url, "--header", "Accept-Encoding: gzip, deflate, br", ]; const { success, stdout } = await new Deno.Command("curl", { args, stderr: "null", stdout: "piped", }).output(); assert(success); const output = decoder.decode(stdout); assert(output.includes("vary: Accept-Encoding\r\n")); assert(output.includes("content-encoding: gzip\r\n")); // Ensure the content-length header is updated (but don't check the exact length). assert(!output.includes(`content-length: ${contentLength}\r\n`)); assert(output.includes("content-length: ")); } await Promise.all([server(), client()]); httpConn!.close(); }, }); Deno.test({ name: "http server compresses when accept-encoding is deflate, gzip", permissions: { net: true, run: true }, async fn() { const hostname = "localhost"; const port = listenPort; let contentLength: string; let httpConn: Deno.HttpConn; async function server() { const listener = Deno.listen({ hostname, port }); const tcpConn = await listener.accept(); httpConn = Deno.serveHttp(tcpConn); const e = await httpConn.nextRequest(); assert(e); const { request, respondWith } = e; assertEquals(request.headers.get("Accept-Encoding"), "deflate, gzip"); const body = "x".repeat(10000); contentLength = String(body.length); const response = new Response( body, { headers: { "content-length": contentLength, }, }, ); await respondWith(response); listener.close(); } async function client() { const url = `http://${hostname}:${port}/`; const cmd = [ "curl", "-i", "--request", "GET", "--url", url, // "--compressed", // Windows curl does not support --compressed "--header", "Accept-Encoding: deflate, gzip", ]; const proc = Deno.run({ cmd, stdout: "piped", stderr: "null" }); const status = await proc.status(); assert(status.success); const output = decoder.decode(await proc.output()); assert(output.includes("vary: Accept-Encoding\r\n")); assert(output.includes("content-encoding: gzip\r\n")); // Ensure the content-length header is updated. assert(!output.includes(`content-length: ${contentLength}\r\n`)); assert(output.includes("content-length: ")); proc.close(); } await Promise.all([server(), client()]); httpConn!.close(); }, }); Deno.test({ name: "http server custom content-encoding is left untouched", permissions: { net: true, run: true }, async fn() { const hostname = "localhost"; const port = listenPort; let contentLength: string; let httpConn: Deno.HttpConn; async function server() { const listener = Deno.listen({ hostname, port }); const tcpConn = await listener.accept(); httpConn = Deno.serveHttp(tcpConn); const e = await httpConn.nextRequest(); assert(e); const { request, respondWith } = e; assertEquals(request.headers.get("Accept-Encoding"), "deflate, gzip"); const body = new Uint8Array([3, 1, 4, 1]); contentLength = String(body.length); const response = new Response( body, { headers: { "content-length": contentLength, "content-encoding": "arbitrary", }, }, ); await respondWith(response); listener.close(); } async function client() { const url = `http://${hostname}:${port}/`; const cmd = [ "curl", "-i", "--request", "GET", "--url", url, // "--compressed", // Windows curl does not support --compressed "--header", "Accept-Encoding: deflate, gzip", ]; const proc = Deno.run({ cmd, stdout: "piped", stderr: "null" }); const status = await proc.status(); assert(status.success); const output = decoder.decode(await proc.output()); assert(output.includes("vary: Accept-Encoding\r\n")); assert(output.includes("content-encoding: arbitrary\r\n")); proc.close(); } await Promise.all([server(), client()]); httpConn!.close(); }, }); Deno.test( { permissions: { net: true } }, async function httpServerReadLargeBodyWithContentLength() { const TLS_PACKET_SIZE = 16 * 1024 + 256; // We want the body to be read in multiple packets const body = "aa\n" + "deno.land large body\n".repeat(TLS_PACKET_SIZE) + "zz"; let httpConn: Deno.HttpConn; const promise = (async () => { const listener = Deno.listen({ port: listenPort }); const conn = await listener.accept(); listener.close(); httpConn = Deno.serveHttp(conn); const reqEvent = await httpConn.nextRequest(); assert(reqEvent); const { request, respondWith } = reqEvent; assertEquals(await request.text(), body); await respondWith(new Response(body)); })(); const resp = await fetch(`http://127.0.0.1:${listenPort}/`, { method: "POST", headers: { "connection": "close" }, body, }); const text = await resp.text(); assertEquals(text, body); await promise; httpConn!.close(); }, ); Deno.test( { permissions: { net: true } }, async function httpServerReadLargeBodyWithTransferChunked() { const TLS_PACKET_SIZE = 16 * 1024 + 256; // We want the body to be read in multiple packets const chunks = [ "aa\n", "deno.land large body\n".repeat(TLS_PACKET_SIZE), "zz", ]; const body = chunks.join(""); const stream = new TransformStream(); const writer = stream.writable.getWriter(); for (const chunk of chunks) { writer.write(new TextEncoder().encode(chunk)); } writer.close(); let httpConn: Deno.HttpConn; const promise = (async () => { const listener = Deno.listen({ port: listenPort }); const conn = await listener.accept(); listener.close(); httpConn = Deno.serveHttp(conn); const reqEvent = await httpConn.nextRequest(); assert(reqEvent); const { request, respondWith } = reqEvent; assertEquals(await request.text(), body); await respondWith(new Response(body)); })(); const resp = await fetch(`http://127.0.0.1:${listenPort}/`, { method: "POST", headers: { "connection": "close" }, body: stream.readable, }); const text = await resp.text(); assertEquals(text, body); await promise; httpConn!.close(); }, ); Deno.test( { permissions: { net: true }, }, async function httpServerWithoutExclusiveAccessToTcp() { const port = listenPort; const listener = Deno.listen({ port }); const [clientConn, serverConn] = await Promise.all([ Deno.connect({ port }), listener.accept(), ]); const buf = new Uint8Array(128); const readPromise = serverConn.read(buf); assertThrows(() => Deno.serveHttp(serverConn), Deno.errors.BadResource); clientConn.close(); listener.close(); await readPromise; }, ); Deno.test( { permissions: { net: true, read: true }, }, async function httpServerWithoutExclusiveAccessToTls() { const hostname = "localhost"; const port = listenPort; const listener = Deno.listenTls({ hostname, port, cert: await Deno.readTextFile("tests/testdata/tls/localhost.crt"), key: await Deno.readTextFile("tests/testdata/tls/localhost.key"), }); const caCerts = [ await Deno.readTextFile("tests/testdata/tls/RootCA.pem"), ]; const [clientConn, serverConn] = await Promise.all([ Deno.connectTls({ hostname, port, caCerts }), listener.accept(), ]); await Promise.all([clientConn.handshake(), serverConn.handshake()]); const buf = new Uint8Array(128); const readPromise = serverConn.read(buf); assertThrows(() => Deno.serveHttp(serverConn), Deno.errors.BadResource); clientConn.close(); listener.close(); await readPromise; }, ); Deno.test( { ignore: Deno.build.os === "windows", permissions: { read: true, write: true }, }, async function httpServerWithoutExclusiveAccessToUnixSocket() { const filePath = tmpUnixSocketPath(); const listener = Deno.listen({ path: filePath, transport: "unix" }); const [clientConn, serverConn] = await Promise.all([ Deno.connect({ path: filePath, transport: "unix" }), listener.accept(), ]); const buf = new Uint8Array(128); const readPromise = serverConn.read(buf); assertThrows(() => Deno.serveHttp(serverConn), Deno.errors.BadResource); clientConn.close(); listener.close(); await readPromise; }, ); Deno.test( { permissions: { net: true } }, async function httpServerRequestResponseClone() { const body = "deno".repeat(64 * 1024); let httpConn: Deno.HttpConn; const listener = Deno.listen({ port: listenPort }); const promise = (async () => { const conn = await listener.accept(); listener.close(); httpConn = Deno.serveHttp(conn); const reqEvent = await httpConn.nextRequest(); assert(reqEvent); const { request, respondWith } = reqEvent; const clone = request.clone(); const reader = clone.body!.getReader(); // get first chunk from branch2 const clonedChunks = []; const { value, done } = await reader.read(); assert(!done); clonedChunks.push(value); // consume request after first chunk single read // readAll should read correctly the rest of the body. // firstChunk should be in the stream internal buffer const body1 = await request.text(); while (true) { const { value, done } = await reader.read(); if (done) break; clonedChunks.push(value); } let offset = 0; const body2 = new Uint8Array(body.length); for (const chunk of clonedChunks) { body2.set(chunk, offset); offset += chunk.byteLength; } assertEquals(body1, body); assertEquals(body1, new TextDecoder().decode(body2)); await respondWith(new Response(body)); })(); const response = await fetch(`http://localhost:${listenPort}`, { body, method: "POST", }); const clone = response.clone(); assertEquals(await response.text(), await clone.text()); await promise; httpConn!.close(); }, ); Deno.test({ name: "http server compresses and flushes each chunk of a streamed resource", permissions: { net: true, run: true }, async fn() { const hostname = "localhost"; const port = listenPort; const port2 = listenPort2; const encoder = new TextEncoder(); const listener = Deno.listen({ hostname, port }); const listener2 = Deno.listen({ hostname, port: port2 }); let httpConn: Deno.HttpConn; async function server() { const tcpConn = await listener.accept(); httpConn = Deno.serveHttp(tcpConn); const e = await httpConn.nextRequest(); assert(e); const { request, respondWith } = e; assertEquals(request.headers.get("Accept-Encoding"), "gzip, deflate, br"); const resp = await fetch(`http://${hostname}:${port2}/`); await respondWith(resp); listener.close(); } const ts = new TransformStream(); const writer = ts.writable.getWriter(); writer.write(encoder.encode("hello")); let httpConn2: Deno.HttpConn; async function server2() { const tcpConn = await listener2.accept(); httpConn2 = Deno.serveHttp(tcpConn); const e = await httpConn2.nextRequest(); assert(e); await e.respondWith( new Response(ts.readable, { headers: { "Content-Type": "text/plain" }, }), ); listener2.close(); } async function client() { const url = `http://${hostname}:${port}/`; const args = [ "--request", "GET", "--url", url, "--header", "Accept-Encoding: gzip, deflate, br", "--no-buffer", ]; const proc = new Deno.Command("curl", { args, stderr: "null", stdout: "piped", }).spawn(); const stdout = proc.stdout .pipeThrough(new DecompressionStream("gzip")) .pipeThrough(new TextDecoderStream()); let body = ""; for await (const chunk of stdout) { body += chunk; if (body === "hello") { writer.write(encoder.encode(" world")); writer.close(); } } assertEquals(body, "hello world"); const status = await proc.status; assert(status.success); } await Promise.all([server(), server2(), client()]); httpConn!.close(); httpConn2!.close(); }, }); Deno.test("case insensitive comma value finder", async (t) => { const cases = /** @type {[string, boolean][]} */ ([ ["websocket", true], ["wEbSOcKET", true], [",wEbSOcKET", true], [",wEbSOcKET,", true], [", wEbSOcKET ,", true], ["test, wEbSOcKET ,", true], ["test ,\twEbSOcKET\t\t ,", true], ["test , wEbSOcKET", true], ["test, asdf,web,wEbSOcKET", true], ["test, asdf,web,wEbSOcKETs", false], ["test, asdf,awebsocket,wEbSOcKETs", false], ]); const findValue = buildCaseInsensitiveCommaValueFinder("websocket"); for (const [input, expected] of cases) { await t.step(input.toString(), () => { const actual = findValue(input); assertEquals(actual, expected); }); } }); async function httpServerWithErrorBody( listener: Deno.Listener, compression: boolean, ): Promise { const conn = await listener.accept(); listener.close(); const httpConn = Deno.serveHttp(conn); const e = await httpConn.nextRequest(); assert(e); const { respondWith } = e; const originalErr = new Error("boom"); const rs = new ReadableStream({ async start(controller) { controller.enqueue(new Uint8Array([65])); await delay(1000); controller.error(originalErr); }, }); const init = compression ? { headers: { "content-type": "text/plain" } } : {}; const response = new Response(rs, init); const err = await assertRejects(() => respondWith(response)); assert(err === originalErr); return httpConn; } for (const compression of [true, false]) { Deno.test({ name: `http server errors stream if response body errors (http/1.1${ compression ? " + compression" : "" })`, permissions: { net: true }, async fn() { const hostname = "localhost"; const port = listenPort; const listener = Deno.listen({ hostname, port }); const server = httpServerWithErrorBody(listener, compression); const conn = await Deno.connect({ hostname, port }); const msg = new TextEncoder().encode( `GET / HTTP/1.1\r\nHost: ${hostname}:${port}\r\n\r\n`, ); const nwritten = await conn.write(msg); assertEquals(nwritten, msg.byteLength); const buf = new Uint8Array(1024); const nread = await conn.read(buf); assert(nread); const data = new TextDecoder().decode(buf.subarray(0, nread)); assert(data.endsWith("1\r\nA\r\n")); const nread2 = await conn.read(buf); // connection should be closed now because the stream errored assertEquals(nread2, null); conn.close(); const httpConn = await server; httpConn.close(); }, }); Deno.test({ name: `http server errors stream if response body errors (http/1.1 + fetch${ compression ? " + compression" : "" })`, permissions: { net: true }, async fn() { const hostname = "localhost"; const port = listenPort; const listener = Deno.listen({ hostname, port }); const server = httpServerWithErrorBody(listener, compression); const resp = await fetch(`http://${hostname}:${port}/`); assert(resp.body); const reader = resp.body.getReader(); const result = await reader.read(); assert(!result.done); assertEquals(result.value, new Uint8Array([65])); const err = await assertRejects(() => reader.read()); assert(err instanceof TypeError); assert(err.message.includes("unexpected EOF")); const httpConn = await server; httpConn.close(); }, }); Deno.test({ name: `http server errors stream if response body errors (http/2 + fetch${ compression ? " + compression" : "" }))`, permissions: { net: true, read: true }, async fn() { const hostname = "localhost"; const port = listenPort; const listener = Deno.listenTls({ hostname, port, cert: await Deno.readTextFile("tests/testdata/tls/localhost.crt"), key: await Deno.readTextFile("tests/testdata/tls/localhost.key"), alpnProtocols: ["h2"], }); const server = httpServerWithErrorBody(listener, compression); const caCert = Deno.readTextFileSync("tests/testdata/tls/RootCA.pem"); const client = Deno.createHttpClient({ caCerts: [caCert] }); const resp = await fetch(`https://${hostname}:${port}/`, { client }); client.close(); assert(resp.body); const reader = resp.body.getReader(); const result = await reader.read(); assert(!result.done); assertEquals(result.value, new Uint8Array([65])); const err = await assertRejects(() => reader.read()); assert(err instanceof TypeError); assert(err.message.includes("unexpected internal error encountered")); const httpConn = await server; httpConn.close(); }, }); } Deno.test({ name: "request signal is aborted when response errors", permissions: { net: true }, async fn() { let httpConn: Deno.HttpConn; const promise = (async () => { const listener = Deno.listen({ port: listenPort }); const conn = await listener.accept(); listener.close(); httpConn = Deno.serveHttp(conn); const ev = await httpConn.nextRequest(); const { request, respondWith } = ev!; await delay(300); await assertRejects(() => respondWith(new Response("Hello World"))); assert(request.signal.aborted); })(); const abortController = new AbortController(); fetch(`http://127.0.0.1:${listenPort}/`, { signal: abortController.signal, }).catch(() => { // ignore }); await delay(100); abortController.abort(); await promise; httpConn!.close(); }, }); Deno.test( async function httpConnExplicitResourceManagement() { let promise; { const listen = Deno.listen({ port: listenPort }); promise = fetch(`http://localhost:${listenPort}/`).catch(() => null); const serverConn = await listen.accept(); listen.close(); using _httpConn = Deno.serveHttp(serverConn); } const response = await promise; assertEquals(response, null); }, ); Deno.test("proxy with fetch", async () => { const listener = Deno.listen({ port: listenPort }); const deferred = Promise.withResolvers(); const server = Deno.serve({ port: listenPort + 1 }, (_req) => { return new Response("Hello world"); }); let httpConn: Deno.HttpConn; async function handleHttp(conn: Deno.Conn) { httpConn = Deno.serveHttp(conn); for await (const e of httpConn) { await e.respondWith(serve(e.request)); break; } } async function serve(req: Request) { return await fetch(`http://localhost:${listenPort + 1}/`, req); } const originServer = (async () => { for await (const conn of listener) { handleHttp(conn); break; } })(); const proxiedRequest = (async () => { const conn = await Deno.connect({ port: listenPort }); const payload = new TextEncoder().encode( "POST /api/sessions HTTP/1.1\x0d\x0aConnection: keep-alive\x0d\x0aContent-Length: 2\x0d\x0a\x0d\x0a{}", ); const n = await conn.write(payload); assertEquals(n, 76); const buf = new Uint8Array(1000); const nread = await conn.read(buf); assertEquals(nread, 150); const respText = new TextDecoder().decode(buf); assert(respText.includes("HTTP/1.1 200 OK")); assert(respText.includes("content-type: text/plain;charset=UTF-8")); assert(respText.includes("vary: Accept-Encoding")); assert(respText.includes("content-length: 11")); assert(respText.includes("Hello world")); conn.close(); deferred.resolve(); })(); await proxiedRequest; await originServer; await deferred.promise; await server.shutdown(); await server.finished; httpConn!.close(); }); function chunkedBodyReader(h: Headers, r: BufReader): Deno.Reader { // Based on https://tools.ietf.org/html/rfc2616#section-19.4.6 const tp = new TextProtoReader(r); let finished = false; const chunks: Array<{ offset: number; data: Uint8Array; }> = []; async function read(buf: Uint8Array): Promise { if (finished) return null; const [chunk] = chunks; if (chunk) { const chunkRemaining = chunk.data.byteLength - chunk.offset; const readLength = Math.min(chunkRemaining, buf.byteLength); for (let i = 0; i < readLength; i++) { buf[i] = chunk.data[chunk.offset + i]; } chunk.offset += readLength; if (chunk.offset === chunk.data.byteLength) { chunks.shift(); // Consume \r\n; if ((await tp.readLine()) === null) { throw new Deno.errors.UnexpectedEof(); } } return readLength; } const line = await tp.readLine(); if (line === null) throw new Deno.errors.UnexpectedEof(); // TODO(bartlomieju): handle chunk extension const [chunkSizeString] = line.split(";"); const chunkSize = parseInt(chunkSizeString, 16); if (Number.isNaN(chunkSize) || chunkSize < 0) { throw new Deno.errors.InvalidData("Invalid chunk size"); } if (chunkSize > 0) { if (chunkSize > buf.byteLength) { let eof = await r.readFull(buf); if (eof === null) { throw new Deno.errors.UnexpectedEof(); } const restChunk = new Uint8Array(chunkSize - buf.byteLength); eof = await r.readFull(restChunk); if (eof === null) { throw new Deno.errors.UnexpectedEof(); } else { chunks.push({ offset: 0, data: restChunk, }); } return buf.byteLength; } else { const bufToFill = buf.subarray(0, chunkSize); const eof = await r.readFull(bufToFill); if (eof === null) { throw new Deno.errors.UnexpectedEof(); } // Consume \r\n if ((await tp.readLine()) === null) { throw new Deno.errors.UnexpectedEof(); } return chunkSize; } } else { assert(chunkSize === 0); // Consume \r\n if ((await r.readLine()) === null) { throw new Deno.errors.UnexpectedEof(); } await readTrailers(h, r); finished = true; return null; } } return { read }; } async function readTrailers( headers: Headers, r: BufReader, ) { const trailers = parseTrailer(headers.get("trailer")); if (trailers == null) return; const trailerNames = [...trailers.keys()]; const tp = new TextProtoReader(r); const result = await tp.readMimeHeader(); if (result == null) { throw new Deno.errors.InvalidData("Missing trailer header."); } const undeclared = [...result.keys()].filter( (k) => !trailerNames.includes(k), ); if (undeclared.length > 0) { throw new Deno.errors.InvalidData( `Undeclared trailers: ${Deno.inspect(undeclared)}.`, ); } for (const [k, v] of result) { headers.append(k, v); } const missingTrailers = trailerNames.filter((k) => !result.has(k)); if (missingTrailers.length > 0) { throw new Deno.errors.InvalidData( `Missing trailers: ${Deno.inspect(missingTrailers)}.`, ); } headers.delete("trailer"); } function parseTrailer(field: string | null): Headers | undefined { if (field == null) { return undefined; } const trailerNames = field.split(",").map((v) => v.trim().toLowerCase()); if (trailerNames.length === 0) { throw new Deno.errors.InvalidData("Empty trailer header."); } const prohibited = trailerNames.filter((k) => isProhibitedForTrailer(k)); if (prohibited.length > 0) { throw new Deno.errors.InvalidData( `Prohibited trailer names: ${Deno.inspect(prohibited)}.`, ); } return new Headers(trailerNames.map((key) => [key, ""])); } function isProhibitedForTrailer(key: string): boolean { const s = new Set(["transfer-encoding", "content-length", "trailer"]); return s.has(key.toLowerCase()); }