diff options
author | Matt Mastracci <matthew@mastracci.com> | 2024-02-10 13:22:13 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-02-10 20:22:13 +0000 |
commit | f5e46c9bf2f50d66a953fa133161fc829cecff06 (patch) | |
tree | 8faf2f5831c1c7b11d842cd9908d141082c869a5 /tests/unit/serve_test.ts | |
parent | d2477f780630a812bfd65e3987b70c0d309385bb (diff) |
chore: move cli/tests/ -> tests/ (#22369)
This looks like a massive PR, but it's only a move from cli/tests ->
tests, and updates of relative paths for files.
This is the first step towards aggregate all of the integration test
files under tests/, which will lead to a set of integration tests that
can run without the CLI binary being built.
While we could leave these tests under `cli`, it would require us to
keep a more complex directory structure for the various test runners. In
addition, we have a lot of complexity to ignore various test files in
the `cli` project itself (cargo publish exclusion rules, autotests =
false, etc).
And finally, the `tests/` folder will eventually house the `test_ffi`,
`test_napi` and other testing code, reducing the size of the root repo
directory.
For easier review, the extremely large and noisy "move" is in the first
commit (with no changes -- just a move), while the remainder of the
changes to actual files is in the second commit.
Diffstat (limited to 'tests/unit/serve_test.ts')
-rw-r--r-- | tests/unit/serve_test.ts | 3932 |
1 files changed, 3932 insertions, 0 deletions
diff --git a/tests/unit/serve_test.ts b/tests/unit/serve_test.ts new file mode 100644 index 000000000..e972b36cd --- /dev/null +++ b/tests/unit/serve_test.ts @@ -0,0 +1,3932 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +import { assertMatch, assertRejects } from "@test_util/std/assert/mod.ts"; +import { Buffer, BufReader, BufWriter } from "@test_util/std/io/mod.ts"; +import { TextProtoReader } from "../testdata/run/textproto.ts"; +import { + assert, + assertEquals, + assertStringIncludes, + assertThrows, + execCode, + fail, + tmpUnixSocketPath, +} from "./test_util.ts"; + +// Since these tests may run in parallel, ensure this port is unique to this file +const servePort = 4502; + +const { + upgradeHttpRaw, + addTrailers, + serveHttpOnListener, + serveHttpOnConnection, + // @ts-expect-error TypeScript (as of 3.7) does not support indexing namespaces by symbol +} = Deno[Deno.internal]; + +function createOnErrorCb(ac: AbortController): (err: unknown) => Response { + return (err) => { + console.error(err); + ac.abort(); + return new Response("Internal server error", { status: 500 }); + }; +} + +function onListen( + resolve: (value: void | PromiseLike<void>) => void, +): ({ hostname, port }: { hostname: string; port: number }) => void { + return () => { + resolve(); + }; +} + +async function makeServer( + handler: (req: Request) => Response | Promise<Response>, +): Promise< + { + finished: Promise<void>; + abort: () => void; + shutdown: () => Promise<void>; + [Symbol.asyncDispose](): PromiseLike<void>; + } +> { + const ac = new AbortController(); + const { promise, resolve } = Promise.withResolvers<void>(); + + const server = Deno.serve({ + handler, + port: servePort, + signal: ac.signal, + onListen: onListen(resolve), + }); + + await promise; + return { + finished: server.finished, + abort() { + ac.abort(); + }, + async shutdown() { + await server.shutdown(); + }, + [Symbol.asyncDispose]() { + return server[Symbol.asyncDispose](); + }, + }; +} + +Deno.test(async function httpServerShutsDownPortBeforeResolving() { + const { finished, abort } = await makeServer((_req) => new Response("ok")); + assertThrows(() => Deno.listen({ port: servePort })); + abort(); + await finished; + + const listener = Deno.listen({ port: servePort }); + listener!.close(); +}); + +// When shutting down abruptly, we require that all in-progress connections are aborted, +// no new connections are allowed, and no new transactions are allowed on existing connections. +Deno.test( + { permissions: { net: true } }, + async function httpServerShutdownAbruptGuaranteeHttp11() { + const deferredQueue: { + input: ReturnType<typeof Promise.withResolvers<string>>; + out: ReturnType<typeof Promise.withResolvers<void>>; + }[] = []; + const { finished, abort } = await makeServer((_req) => { + const { input, out } = deferredQueue.shift()!; + return new Response( + new ReadableStream({ + async start(controller) { + controller.enqueue(new Uint8Array([46])); + out.resolve(); + controller.enqueue(encoder.encode(await input.promise)); + controller.close(); + }, + }), + ); + }); + const encoder = new TextEncoder(); + const decoder = new TextDecoder(); + const conn = await Deno.connect({ port: servePort }); + const w = conn.writable.getWriter(); + const r = conn.readable.getReader(); + + const deferred1 = { + input: Promise.withResolvers<string>(), + out: Promise.withResolvers<void>(), + }; + deferredQueue.push(deferred1); + const deferred2 = { + input: Promise.withResolvers<string>(), + out: Promise.withResolvers<void>(), + }; + deferredQueue.push(deferred2); + const deferred3 = { + input: Promise.withResolvers<string>(), + out: Promise.withResolvers<void>(), + }; + deferredQueue.push(deferred3); + deferred1.input.resolve("#"); + deferred2.input.resolve("$"); + await w.write(encoder.encode(`GET / HTTP/1.1\nConnection: keep-alive\n\n`)); + await w.write(encoder.encode(`GET / HTTP/1.1\nConnection: keep-alive\n\n`)); + + // Fully read two responses + let text = ""; + while (!text.includes("$\r\n")) { + text += decoder.decode((await r.read()).value); + } + + await w.write(encoder.encode(`GET / HTTP/1.1\nConnection: keep-alive\n\n`)); + await deferred3.out.promise; + + // This is half served, so wait for the chunk that has the first '.' + text = ""; + while (!text.includes("1\r\n.\r\n")) { + text += decoder.decode((await r.read()).value); + } + + abort(); + + // This doesn't actually write anything, but we release it after aborting + deferred3.input.resolve("!"); + + // Guarantee: can't connect to an aborted server (though this may not happen immediately) + let failed = false; + for (let i = 0; i < 10; i++) { + try { + const conn = await Deno.connect({ port: servePort }); + conn.close(); + // Give the runtime a few ticks to settle (required for Windows) + await new Promise((r) => setTimeout(r, 2 ** i)); + continue; + } catch (_) { + failed = true; + break; + } + } + assert(failed, "The Deno.serve listener was not disabled promptly"); + + // Guarantee: the pipeline is closed abruptly + assert((await r.read()).done); + + try { + conn.close(); + } catch (_) { + // Ignore + } + await finished; + }, +); + +// When shutting down abruptly, we require that all in-progress connections are aborted, +// no new connections are allowed, and no new transactions are allowed on existing connections. +Deno.test( + { permissions: { net: true } }, + async function httpServerShutdownGracefulGuaranteeHttp11() { + const deferredQueue: { + input: ReturnType<typeof Promise.withResolvers<string>>; + out: ReturnType<typeof Promise.withResolvers<void>>; + }[] = []; + const { finished, shutdown } = await makeServer((_req) => { + const { input, out } = deferredQueue.shift()!; + return new Response( + new ReadableStream({ + async start(controller) { + controller.enqueue(new Uint8Array([46])); + out.resolve(); + controller.enqueue(encoder.encode(await input.promise)); + controller.close(); + }, + }), + ); + }); + const encoder = new TextEncoder(); + const decoder = new TextDecoder(); + const conn = await Deno.connect({ port: servePort }); + const w = conn.writable.getWriter(); + const r = conn.readable.getReader(); + + const deferred1 = { + input: Promise.withResolvers<string>(), + out: Promise.withResolvers<void>(), + }; + deferredQueue.push(deferred1); + const deferred2 = { + input: Promise.withResolvers<string>(), + out: Promise.withResolvers<void>(), + }; + deferredQueue.push(deferred2); + const deferred3 = { + input: Promise.withResolvers<string>(), + out: Promise.withResolvers<void>(), + }; + deferredQueue.push(deferred3); + deferred1.input.resolve("#"); + deferred2.input.resolve("$"); + await w.write(encoder.encode(`GET / HTTP/1.1\nConnection: keep-alive\n\n`)); + await w.write(encoder.encode(`GET / HTTP/1.1\nConnection: keep-alive\n\n`)); + + // Fully read two responses + let text = ""; + while (!text.includes("$\r\n")) { + text += decoder.decode((await r.read()).value); + } + + await w.write(encoder.encode(`GET / HTTP/1.1\nConnection: keep-alive\n\n`)); + await deferred3.out.promise; + + // This is half served, so wait for the chunk that has the first '.' + text = ""; + while (!text.includes("1\r\n.\r\n")) { + text += decoder.decode((await r.read()).value); + } + + const shutdownPromise = shutdown(); + + // Release the final response _after_ we shut down + deferred3.input.resolve("!"); + + // Guarantee: can't connect to an aborted server (though this may not happen immediately) + let failed = false; + for (let i = 0; i < 10; i++) { + try { + const conn = await Deno.connect({ port: servePort }); + conn.close(); + // Give the runtime a few ticks to settle (required for Windows) + await new Promise((r) => setTimeout(r, 2 ** i)); + continue; + } catch (_) { + failed = true; + break; + } + } + assert(failed, "The Deno.serve listener was not disabled promptly"); + + // Guarantee: existing connections fully drain + while (!text.includes("!\r\n")) { + text += decoder.decode((await r.read()).value); + } + + await shutdownPromise; + + try { + conn.close(); + } catch (_) { + // Ignore + } + await finished; + }, +); + +// Ensure that resources don't leak during a graceful shutdown +Deno.test( + { permissions: { net: true, write: true, read: true } }, + async function httpServerShutdownGracefulResources() { + const { promise, resolve } = Promise.withResolvers<void>(); + const { finished, shutdown } = await makeServer(async (_req) => { + resolve(); + await new Promise((r) => setTimeout(r, 10)); + return new Response((await makeTempFile(1024 * 1024)).readable); + }); + + const f = fetch(`http://localhost:${servePort}`); + await promise; + assertEquals((await (await f).text()).length, 1048576); + await shutdown(); + await finished; + }, +); + +// Ensure that resources don't leak during a graceful shutdown +Deno.test( + { permissions: { net: true, write: true, read: true } }, + async function httpServerShutdownGracefulResources2() { + const waitForAbort = Promise.withResolvers<void>(); + const waitForRequest = Promise.withResolvers<void>(); + const { finished, shutdown } = await makeServer(async (_req) => { + waitForRequest.resolve(); + await waitForAbort.promise; + await new Promise((r) => setTimeout(r, 10)); + return new Response((await makeTempFile(1024 * 1024)).readable); + }); + + const f = fetch(`http://localhost:${servePort}`); + await waitForRequest.promise; + const s = shutdown(); + waitForAbort.resolve(); + assertEquals((await (await f).text()).length, 1048576); + await s; + await finished; + }, +); + +Deno.test( + { permissions: { net: true, write: true, read: true } }, + async function httpServerExplicitResourceManagement() { + let dataPromise; + + { + await using _server = await makeServer(async (_req) => { + return new Response((await makeTempFile(1024 * 1024)).readable); + }); + + const resp = await fetch(`http://localhost:${servePort}`); + dataPromise = resp.arrayBuffer(); + } + + assertEquals((await dataPromise).byteLength, 1048576); + }, +); + +Deno.test( + { permissions: { net: true, write: true, read: true } }, + async function httpServerExplicitResourceManagementManualClose() { + await using server = await makeServer(async (_req) => { + return new Response((await makeTempFile(1024 * 1024)).readable); + }); + + const resp = await fetch(`http://localhost:${servePort}`); + + const [_, data] = await Promise.all([ + server.shutdown(), + resp.arrayBuffer(), + ]); + + assertEquals(data.byteLength, 1048576); + }, +); + +Deno.test( + { permissions: { read: true, run: true } }, + async function httpServerUnref() { + const [statusCode, _output] = await execCode(` + async function main() { + const server = Deno.serve({ port: ${servePort}, handler: () => null }); + server.unref(); + await server.finished; // This doesn't block the program from exiting + } + main(); + `); + assertEquals(statusCode, 0); + }, +); + +Deno.test(async function httpServerCanResolveHostnames() { + const ac = new AbortController(); + const { promise, resolve } = Promise.withResolvers<void>(); + + const server = Deno.serve({ + handler: (_req) => new Response("ok"), + hostname: "localhost", + port: servePort, + signal: ac.signal, + onListen: onListen(resolve), + onError: createOnErrorCb(ac), + }); + + await promise; + const resp = await fetch(`http://localhost:${servePort}/`, { + headers: { "connection": "close" }, + }); + const text = await resp.text(); + assertEquals(text, "ok"); + ac.abort(); + await server.finished; +}); + +Deno.test(async function httpServerRejectsOnAddrInUse() { + const ac = new AbortController(); + const { promise, resolve } = Promise.withResolvers<void>(); + + const server = Deno.serve({ + handler: (_req) => new Response("ok"), + hostname: "localhost", + port: servePort, + signal: ac.signal, + onListen: onListen(resolve), + onError: createOnErrorCb(ac), + }); + await promise; + + assertThrows( + () => + Deno.serve({ + handler: (_req) => new Response("ok"), + hostname: "localhost", + port: servePort, + signal: ac.signal, + onListen: onListen(resolve), + onError: createOnErrorCb(ac), + }), + Deno.errors.AddrInUse, + ); + ac.abort(); + await server.finished; +}); + +Deno.test({ permissions: { net: true } }, async function httpServerBasic() { + const ac = new AbortController(); + const deferred = Promise.withResolvers<void>(); + const listeningDeferred = Promise.withResolvers<void>(); + + const server = Deno.serve({ + handler: async (request, { remoteAddr }) => { + // FIXME(bartlomieju): + // make sure that request can be inspected + console.log(request); + assertEquals(new URL(request.url).href, `http://127.0.0.1:${servePort}/`); + assertEquals(await request.text(), ""); + assertEquals(remoteAddr.hostname, "127.0.0.1"); + deferred.resolve(); + return new Response("Hello World", { headers: { "foo": "bar" } }); + }, + port: servePort, + signal: ac.signal, + onListen: onListen(listeningDeferred.resolve), + onError: createOnErrorCb(ac), + }); + + await listeningDeferred.promise; + const resp = await fetch(`http://127.0.0.1:${servePort}/`, { + headers: { "connection": "close" }, + }); + await deferred.promise; + 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"); + ac.abort(); + await server.finished; +}); + +// Test serving of HTTP on an arbitrary listener. +Deno.test( + { permissions: { net: true } }, + async function httpServerOnListener() { + const ac = new AbortController(); + const deferred = Promise.withResolvers<void>(); + const listeningDeferred = Promise.withResolvers<void>(); + const listener = Deno.listen({ port: servePort }); + const server = serveHttpOnListener( + listener, + ac.signal, + async ( + request: Request, + { remoteAddr }: { remoteAddr: { hostname: string } }, + ) => { + assertEquals( + new URL(request.url).href, + `http://127.0.0.1:${servePort}/`, + ); + assertEquals(await request.text(), ""); + assertEquals(remoteAddr.hostname, "127.0.0.1"); + deferred.resolve(); + return new Response("Hello World", { headers: { "foo": "bar" } }); + }, + createOnErrorCb(ac), + onListen(listeningDeferred.resolve), + ); + + await listeningDeferred.promise; + const resp = await fetch(`http://127.0.0.1:${servePort}/`, { + headers: { "connection": "close" }, + }); + await listeningDeferred.promise; + 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"); + ac.abort(); + await server.finished; + }, +); + +// Test serving of HTTP on an arbitrary connection. +Deno.test( + { permissions: { net: true } }, + async function httpServerOnConnection() { + const ac = new AbortController(); + const deferred = Promise.withResolvers<void>(); + const listeningDeferred = Promise.withResolvers<void>(); + const listener = Deno.listen({ port: servePort }); + const acceptPromise = listener.accept(); + const fetchPromise = fetch(`http://127.0.0.1:${servePort}/`, { + headers: { "connection": "close" }, + }); + + const server = serveHttpOnConnection( + await acceptPromise, + ac.signal, + async ( + request: Request, + { remoteAddr }: { remoteAddr: { hostname: string } }, + ) => { + assertEquals( + new URL(request.url).href, + `http://127.0.0.1:${servePort}/`, + ); + assertEquals(await request.text(), ""); + assertEquals(remoteAddr.hostname, "127.0.0.1"); + deferred.resolve(); + return new Response("Hello World", { headers: { "foo": "bar" } }); + }, + createOnErrorCb(ac), + onListen(listeningDeferred.resolve), + ); + + const resp = await fetchPromise; + await deferred.promise; + 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"); + // Note that we don't need to abort this server -- it closes when the connection does + // ac.abort(); + await server.finished; + listener.close(); + }, +); + +Deno.test({ permissions: { net: true } }, async function httpServerOnError() { + const ac = new AbortController(); + const { promise, resolve } = Promise.withResolvers<void>(); + let requestStash: Request | null; + + const server = Deno.serve({ + handler: async (request: Request) => { + requestStash = request; + await new Promise((r) => setTimeout(r, 100)); + throw "fail"; + }, + port: servePort, + signal: ac.signal, + onListen: onListen(resolve), + onError: () => { + return new Response("failed: " + requestStash!.url, { status: 500 }); + }, + }); + + await promise; + const resp = await fetch(`http://127.0.0.1:${servePort}/`, { + headers: { "connection": "close" }, + }); + const text = await resp.text(); + ac.abort(); + await server.finished; + + assertEquals(text, `failed: http://127.0.0.1:${servePort}/`); +}); + +Deno.test( + { permissions: { net: true } }, + async function httpServerOnErrorFails() { + const ac = new AbortController(); + const { promise, resolve } = Promise.withResolvers<void>(); + // NOTE(bartlomieju): deno lint doesn't know that it's actually used later, + // but TypeScript can't see that either ¯\_(ツ)_/¯ + // deno-lint-ignore no-unused-vars + let requestStash: Request | null; + + const server = Deno.serve({ + handler: async (request: Request) => { + requestStash = request; + await new Promise((r) => setTimeout(r, 100)); + throw "fail"; + }, + port: servePort, + signal: ac.signal, + onListen: onListen(resolve), + onError: () => { + throw "again"; + }, + }); + + await promise; + const resp = await fetch(`http://127.0.0.1:${servePort}/`, { + headers: { "connection": "close" }, + }); + const text = await resp.text(); + ac.abort(); + await server.finished; + + assertEquals(text, "Internal Server Error"); + }, +); + +Deno.test({ permissions: { net: true } }, async function httpServerOverload1() { + const ac = new AbortController(); + const deferred = Promise.withResolvers<void>(); + const listeningDeferred = Promise.withResolvers<void>(); + + const server = Deno.serve({ + port: servePort, + signal: ac.signal, + onListen: onListen(listeningDeferred.resolve), + onError: createOnErrorCb(ac), + }, async (request) => { + // FIXME(bartlomieju): + // make sure that request can be inspected + console.log(request); + assertEquals(new URL(request.url).href, `http://127.0.0.1:${servePort}/`); + assertEquals(await request.text(), ""); + deferred.resolve(); + return new Response("Hello World", { headers: { "foo": "bar" } }); + }); + + await listeningDeferred.promise; + const resp = await fetch(`http://127.0.0.1:${servePort}/`, { + headers: { "connection": "close" }, + }); + await deferred.promise; + 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"); + ac.abort(); + await server.finished; +}); + +Deno.test({ permissions: { net: true } }, async function httpServerOverload2() { + const ac = new AbortController(); + const deferred = Promise.withResolvers<void>(); + const listeningDeferred = Promise.withResolvers<void>(); + + const server = Deno.serve({ + port: servePort, + signal: ac.signal, + onListen: onListen(listeningDeferred.resolve), + onError: createOnErrorCb(ac), + }, async (request) => { + // FIXME(bartlomieju): + // make sure that request can be inspected + console.log(request); + assertEquals(new URL(request.url).href, `http://127.0.0.1:${servePort}/`); + assertEquals(await request.text(), ""); + deferred.resolve(); + return new Response("Hello World", { headers: { "foo": "bar" } }); + }); + + await listeningDeferred.promise; + const resp = await fetch(`http://127.0.0.1:${servePort}/`, { + headers: { "connection": "close" }, + }); + await deferred.promise; + 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"); + ac.abort(); + await server.finished; +}); + +Deno.test( + { permissions: { net: true } }, + function httpServerErrorOverloadMissingHandler() { + // @ts-ignore - testing invalid overload + assertThrows(() => Deno.serve(), TypeError, "handler"); + // @ts-ignore - testing invalid overload + assertThrows(() => Deno.serve({}), TypeError, "handler"); + assertThrows( + // @ts-ignore - testing invalid overload + () => Deno.serve({ handler: undefined }), + TypeError, + "handler", + ); + assertThrows( + // @ts-ignore - testing invalid overload + () => Deno.serve(undefined, { handler: () => {} }), + TypeError, + "handler", + ); + }, +); + +Deno.test({ permissions: { net: true } }, async function httpServerPort0() { + const ac = new AbortController(); + + const server = Deno.serve({ + handler() { + return new Response("Hello World"); + }, + port: 0, + signal: ac.signal, + onListen({ port }) { + assert(port > 0 && port < 65536); + ac.abort(); + }, + }); + await server.finished; +}); + +Deno.test( + { permissions: { net: true } }, + async function httpServerDefaultOnListenCallback() { + const ac = new AbortController(); + + const consoleLog = console.log; + console.log = (msg) => { + try { + const match = msg.match(/Listening on http:\/\/localhost:(\d+)\//); + assert(!!match, `Didn't match ${msg}`); + const port = +match[1]; + assert(port > 0 && port < 65536); + } finally { + ac.abort(); + } + }; + + try { + const server = Deno.serve({ + handler() { + return new Response("Hello World"); + }, + hostname: "0.0.0.0", + port: 0, + signal: ac.signal, + }); + + await server.finished; + } finally { + console.log = consoleLog; + } + }, +); + +// https://github.com/denoland/deno/issues/15107 +Deno.test( + { permissions: { net: true } }, + async function httpLazyHeadersIssue15107() { + const deferred = Promise.withResolvers<void>(); + const listeningDeferred = Promise.withResolvers<void>(); + const ac = new AbortController(); + + let headers: Headers; + const server = Deno.serve({ + handler: async (request) => { + await request.text(); + headers = request.headers; + deferred.resolve(); + return new Response(""); + }, + port: servePort, + signal: ac.signal, + onListen: onListen(listeningDeferred.resolve), + onError: createOnErrorCb(ac), + }); + + await listeningDeferred.promise; + const conn = await Deno.connect({ port: servePort }); + // 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 deferred.promise; + conn.close(); + assertEquals(headers!.get("content-length"), "5"); + ac.abort(); + await server.finished; + }, +); + +function createUrlTest( + name: string, + methodAndPath: string, + host: string | null, + expected: string, +) { + Deno.test(`httpServerUrl${name}`, async () => { + const listeningDeferred = Promise.withResolvers<number>(); + const urlDeferred = Promise.withResolvers<string>(); + const ac = new AbortController(); + const server = Deno.serve({ + handler: (request: Request) => { + urlDeferred.resolve(request.url); + return new Response(""); + }, + port: 0, + signal: ac.signal, + onListen: ({ port }: { port: number }) => { + listeningDeferred.resolve(port); + }, + onError: createOnErrorCb(ac), + }); + + const port = await listeningDeferred.promise; + const conn = await Deno.connect({ port }); + + const encoder = new TextEncoder(); + const body = `${methodAndPath} HTTP/1.1\r\n${ + host ? ("Host: " + host + "\r\n") : "" + }Content-Length: 5\r\n\r\n12345`; + const writeResult = await conn.write(encoder.encode(body)); + assertEquals(body.length, writeResult); + + try { + const expectedResult = expected.replace("HOST", "localhost").replace( + "PORT", + `${port}`, + ); + assertEquals(await urlDeferred.promise, expectedResult); + } finally { + ac.abort(); + await server.finished; + conn.close(); + } + }); +} + +createUrlTest("WithPath", "GET /path", null, "http://HOST:PORT/path"); +createUrlTest( + "WithPathAndHost", + "GET /path", + "deno.land", + "http://deno.land/path", +); +createUrlTest( + "WithAbsolutePath", + "GET http://localhost/path", + null, + "http://localhost/path", +); +createUrlTest( + "WithAbsolutePathAndHost", + "GET http://localhost/path", + "deno.land", + "http://localhost/path", +); +createUrlTest( + "WithPortAbsolutePath", + "GET http://localhost:1234/path", + null, + "http://localhost:1234/path", +); +createUrlTest( + "WithPortAbsolutePathAndHost", + "GET http://localhost:1234/path", + "deno.land", + "http://localhost:1234/path", +); +createUrlTest( + "WithPortAbsolutePathAndHostWithPort", + "GET http://localhost:1234/path", + "deno.land:9999", + "http://localhost:1234/path", +); + +createUrlTest("WithAsterisk", "OPTIONS *", null, "*"); +createUrlTest( + "WithAuthorityForm", + "CONNECT deno.land:80", + null, + "deno.land:80", +); + +// TODO(mmastrac): These should probably be 400 errors +createUrlTest("WithInvalidAsterisk", "GET *", null, "*"); +createUrlTest("WithInvalidNakedPath", "GET path", null, "path"); +createUrlTest( + "WithInvalidNakedAuthority", + "GET deno.land:1234", + null, + "deno.land:1234", +); + +Deno.test( + { permissions: { net: true } }, + async function httpServerGetRequestBody() { + const deferred = Promise.withResolvers<void>(); + const ac = new AbortController(); + const listeningDeferred = Promise.withResolvers<void>(); + + const server = Deno.serve({ + handler: (request) => { + assertEquals(request.body, null); + deferred.resolve(); + return new Response("", { headers: {} }); + }, + port: servePort, + signal: ac.signal, + onListen: onListen(listeningDeferred.resolve), + onError: createOnErrorCb(ac), + }); + + await listeningDeferred.promise; + const conn = await Deno.connect({ port: servePort }); + // Send GET request with a body + content-length. + const encoder = new TextEncoder(); + const body = + `GET / HTTP/1.1\r\nHost: 127.0.0.1:${servePort}\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); + assert(readResult); + assert(readResult > 0); + + conn.close(); + await deferred.promise; + ac.abort(); + await server.finished; + }, +); + +Deno.test( + { permissions: { net: true } }, + async function httpServerAbortedRequestBody() { + const deferred = Promise.withResolvers<void>(); + const ac = new AbortController(); + const listeningDeferred = Promise.withResolvers<void>(); + + const server = Deno.serve({ + handler: async (request) => { + await assertRejects(async () => { + await request.text(); + }); + deferred.resolve(); + // Not actually used + return new Response(); + }, + port: servePort, + signal: ac.signal, + onListen: onListen(listeningDeferred.resolve), + onError: createOnErrorCb(ac), + }); + + await listeningDeferred.promise; + const conn = await Deno.connect({ port: servePort }); + // Send POST request with a body + content-length, but don't send it all + const encoder = new TextEncoder(); + const body = + `POST / HTTP/1.1\r\nHost: 127.0.0.1:${servePort}\r\nContent-Length: 10\r\n\r\n12345`; + const writeResult = await conn.write(encoder.encode(body)); + assertEquals(body.length, writeResult); + conn.close(); + await deferred.promise; + ac.abort(); + await server.finished; + }, +); + +function createStreamTest(count: number, delay: number, action: string) { + function doAction(controller: ReadableStreamDefaultController, i: number) { + if (i == count) { + if (action == "Throw") { + controller.error(new Error("Expected error!")); + } else { + controller.close(); + } + } else { + controller.enqueue(`a${i}`); + + if (delay == 0) { + doAction(controller, i + 1); + } else { + setTimeout(() => doAction(controller, i + 1), delay); + } + } + } + + function makeStream(_count: number, delay: number): ReadableStream { + return new ReadableStream({ + start(controller) { + if (delay == 0) { + doAction(controller, 0); + } else { + setTimeout(() => doAction(controller, 0), delay); + } + }, + }).pipeThrough(new TextEncoderStream()); + } + + Deno.test(`httpServerStreamCount${count}Delay${delay}${action}`, async () => { + const ac = new AbortController(); + const { promise, resolve } = Promise.withResolvers<void>(); + const server = Deno.serve({ + handler: (_request) => { + return new Response(makeStream(count, delay)); + }, + port: servePort, + signal: ac.signal, + onListen: onListen(resolve), + onError: createOnErrorCb(ac), + }); + + try { + await promise; + const resp = await fetch(`http://127.0.0.1:${servePort}/`); + if (action == "Throw") { + await assertRejects(async () => { + await resp.text(); + }); + } else { + const text = await resp.text(); + + let expected = ""; + for (let i = 0; i < count; i++) { + expected += `a${i}`; + } + + assertEquals(text, expected); + } + } finally { + ac.abort(); + await server.shutdown(); + } + }); +} + +for (const count of [0, 1, 2, 3]) { + for (const delay of [0, 1, 25]) { + // Creating a stream that errors in start will throw + if (delay > 0) { + createStreamTest(count, delay, "Throw"); + } + createStreamTest(count, delay, "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 { promise, resolve } = Promise.withResolvers<void>(); + const ac = new AbortController(); + const server = Deno.serve({ + handler: async (request) => { + const reqBody = await request.text(); + assertEquals("hello world", reqBody); + return new Response("yo"); + }, + port: servePort, + signal: ac.signal, + onListen: onListen(resolve), + onError: createOnErrorCb(ac), + }); + + await promise; + const resp = await fetch(`http://127.0.0.1:${servePort}/`, { + body: stream.readable, + method: "POST", + headers: { "connection": "close" }, + }); + + assertEquals(await resp.text(), "yo"); + ac.abort(); + await server.finished; + }, +); + +Deno.test({ permissions: { net: true } }, async function httpServerClose() { + const ac = new AbortController(); + const { promise, resolve } = Promise.withResolvers<void>(); + const server = Deno.serve({ + handler: () => new Response("ok"), + port: servePort, + signal: ac.signal, + onListen: onListen(resolve), + onError: createOnErrorCb(ac), + }); + await promise; + const client = await Deno.connect({ port: servePort }); + client.close(); + ac.abort(); + await server.finished; +}); + +// https://github.com/denoland/deno/issues/15427 +Deno.test({ permissions: { net: true } }, async function httpServerCloseGet() { + const ac = new AbortController(); + const listeningDeferred = Promise.withResolvers<void>(); + const requestDeferred = Promise.withResolvers<void>(); + const responseDeferred = Promise.withResolvers<void>(); + const server = Deno.serve({ + handler: async () => { + requestDeferred.resolve(); + await new Promise((r) => setTimeout(r, 500)); + responseDeferred.resolve(); + return new Response("ok"); + }, + port: servePort, + signal: ac.signal, + onListen: onListen(listeningDeferred.resolve), + onError: createOnErrorCb(ac), + }); + await listeningDeferred.promise; + const conn = await Deno.connect({ port: servePort }); + const encoder = new TextEncoder(); + const body = + `GET / HTTP/1.1\r\nHost: example.domain\r\nConnection: close\r\n\r\n`; + const writeResult = await conn.write(encoder.encode(body)); + assertEquals(body.length, writeResult); + await requestDeferred.promise; + conn.close(); + await responseDeferred.promise; + ac.abort(); + await server.finished; +}); + +// FIXME: +Deno.test( + { permissions: { net: true } }, + async function httpServerEmptyBlobResponse() { + const ac = new AbortController(); + const { promise, resolve } = Promise.withResolvers<void>(); + const server = Deno.serve({ + handler: () => new Response(new Blob([])), + port: servePort, + signal: ac.signal, + onListen: onListen(resolve), + onError: createOnErrorCb(ac), + }); + + await promise; + const resp = await fetch(`http://127.0.0.1:${servePort}/`); + const respBody = await resp.text(); + + assertEquals("", respBody); + ac.abort(); + await server.finished; + }, +); + +// https://github.com/denoland/deno/issues/17291 +Deno.test( + { permissions: { net: true } }, + async function httpServerIncorrectChunkedResponse() { + const ac = new AbortController(); + const listeningDeferred = Promise.withResolvers<void>(); + const errorDeferred = Promise.withResolvers<void>(); + const server = Deno.serve({ + handler: () => { + const body = new ReadableStream({ + start(controller) { + // Non-encoded string is not a valid readable chunk. + // @ts-ignore we're testing that input is invalid + controller.enqueue("wat"); + }, + type: "bytes", + }); + return new Response(body); + }, + port: servePort, + signal: ac.signal, + onListen: onListen(listeningDeferred.resolve), + onError: (err) => { + const errResp = new Response( + `Internal server error: ${(err as Error).message}`, + { status: 500 }, + ); + errorDeferred.resolve(); + return errResp; + }, + }); + + await listeningDeferred.promise; + const resp = await fetch(`http://127.0.0.1:${servePort}/`); + // Incorrectly implemented reader ReadableStream should reject. + assertStringIncludes(await resp.text(), "Failed to execute 'enqueue'"); + await errorDeferred.promise; + ac.abort(); + await server.finished; + }, +); + +Deno.test( + { permissions: { net: true } }, + async function httpServerCorrectLengthForUnicodeString() { + const ac = new AbortController(); + const { promise, resolve } = Promise.withResolvers<void>(); + + const server = Deno.serve({ + handler: () => new Response("韓國".repeat(10)), + port: servePort, + signal: ac.signal, + onListen: onListen(resolve), + onError: createOnErrorCb(ac), + }); + + await promise; + const conn = await Deno.connect({ port: servePort }); + const encoder = new TextEncoder(); + const decoder = new TextDecoder(); + + const body = + `GET / HTTP/1.1\r\nHost: example.domain\r\nConnection: close\r\n\r\n`; + const writeResult = await conn.write(encoder.encode(body)); + assertEquals(body.length, writeResult); + + const buf = new Uint8Array(1024); + const readResult = await conn.read(buf); + assert(readResult); + const msg = decoder.decode(buf.subarray(0, readResult)); + + conn.close(); + + ac.abort(); + await server.finished; + assert(msg.includes("content-length: 60")); + }, +); + +Deno.test({ permissions: { net: true } }, async function httpServerWebSocket() { + const ac = new AbortController(); + const listeningDeferred = Promise.withResolvers<void>(); + const doneDeferred = Promise.withResolvers<void>(); + const server = Deno.serve({ + handler: (request) => { + const { + response, + socket, + } = Deno.upgradeWebSocket(request); + socket.onerror = (e) => { + console.error(e); + fail(); + }; + socket.onmessage = (m) => { + socket.send(m.data); + socket.close(1001); + }; + socket.onclose = () => doneDeferred.resolve(); + return response; + }, + port: servePort, + signal: ac.signal, + onListen: onListen(listeningDeferred.resolve), + onError: createOnErrorCb(ac), + }); + + await listeningDeferred.promise; + const def = Promise.withResolvers<void>(); + const ws = new WebSocket(`ws://localhost:${servePort}`); + ws.onmessage = (m) => assertEquals(m.data, "foo"); + ws.onerror = (e) => { + console.error(e); + fail(); + }; + ws.onclose = () => def.resolve(); + ws.onopen = () => ws.send("foo"); + + await def.promise; + await doneDeferred.promise; + ac.abort(); + await server.finished; +}); + +Deno.test( + { permissions: { net: true } }, + async function httpServerWebSocketRaw() { + const ac = new AbortController(); + const { promise, resolve } = Promise.withResolvers<void>(); + const server = Deno.serve({ + handler: async (request) => { + const { conn, response } = upgradeHttpRaw(request); + const buf = new Uint8Array(1024); + let read; + + // Write our fake HTTP upgrade + await conn.write( + new TextEncoder().encode( + "HTTP/1.1 101 Switching Protocols\r\nConnection: Upgraded\r\n\r\nExtra", + ), + ); + + // Upgrade data + read = await conn.read(buf); + assertEquals( + new TextDecoder().decode(buf.subarray(0, read!)), + "Upgrade data", + ); + // Read the packet to echo + read = await conn.read(buf); + // Echo + await conn.write(buf.subarray(0, read!)); + + conn.close(); + return response; + }, + port: servePort, + signal: ac.signal, + onListen: onListen(resolve), + onError: createOnErrorCb(ac), + }); + + await promise; + + const conn = await Deno.connect({ port: servePort }); + await conn.write( + new TextEncoder().encode( + "GET / HTTP/1.1\r\nConnection: Upgrade\r\nUpgrade: websocket\r\n\r\nUpgrade data", + ), + ); + const buf = new Uint8Array(1024); + let len; + + // Headers + let headers = ""; + for (let i = 0; i < 2; i++) { + len = await conn.read(buf); + headers += new TextDecoder().decode(buf.subarray(0, len!)); + if (headers.endsWith("Extra")) { + break; + } + } + assertMatch( + headers, + /HTTP\/1\.1 101 Switching Protocols[ ,.A-Za-z:0-9\r\n]*Extra/im, + ); + + // Data to echo + await conn.write(new TextEncoder().encode("buffer data")); + + // Echo + len = await conn.read(buf); + assertEquals( + new TextDecoder().decode(buf.subarray(0, len!)), + "buffer data", + ); + + conn.close(); + ac.abort(); + await server.finished; + }, +); + +Deno.test( + { permissions: { net: true } }, + async function httpServerWebSocketUpgradeTwice() { + const ac = new AbortController(); + const done = Promise.withResolvers<void>(); + const listeningDeferred = Promise.withResolvers<void>(); + const server = Deno.serve({ + handler: (request) => { + const { + response, + socket, + } = Deno.upgradeWebSocket(request); + assertThrows( + () => { + Deno.upgradeWebSocket(request); + }, + Deno.errors.Http, + "already upgraded", + ); + socket.onerror = (e) => { + console.error(e); + fail(); + }; + socket.onmessage = (m) => { + socket.send(m.data); + socket.close(1001); + }; + socket.onclose = () => done.resolve(); + return response; + }, + port: servePort, + signal: ac.signal, + onListen: onListen(listeningDeferred.resolve), + onError: createOnErrorCb(ac), + }); + + await listeningDeferred.promise; + const def = Promise.withResolvers<void>(); + const ws = new WebSocket(`ws://localhost:${servePort}`); + ws.onmessage = (m) => assertEquals(m.data, "foo"); + ws.onerror = (e) => { + console.error(e); + fail(); + }; + ws.onclose = () => def.resolve(); + ws.onopen = () => ws.send("foo"); + + await def.promise; + await done.promise; + ac.abort(); + await server.finished; + }, +); + +Deno.test( + { permissions: { net: true } }, + async function httpServerWebSocketCloseFast() { + const ac = new AbortController(); + const done = Promise.withResolvers<void>(); + const listeningDeferred = Promise.withResolvers<void>(); + const server = Deno.serve({ + handler: (request) => { + const { + response, + socket, + } = Deno.upgradeWebSocket(request); + socket.onopen = () => socket.close(); + socket.onclose = () => done.resolve(); + return response; + }, + port: servePort, + signal: ac.signal, + onListen: onListen(listeningDeferred.resolve), + onError: createOnErrorCb(ac), + }); + + await listeningDeferred.promise; + const def = Promise.withResolvers<void>(); + const ws = new WebSocket(`ws://localhost:${servePort}`); + ws.onerror = (e) => { + console.error(e); + fail(); + }; + ws.onclose = () => def.resolve(); + + await def.promise; + await done.promise; + ac.abort(); + await server.finished; + }, +); + +Deno.test( + { permissions: { net: true } }, + async function httpServerWebSocketCanAccessRequest() { + const ac = new AbortController(); + const done = Promise.withResolvers<void>(); + const listeningDeferred = Promise.withResolvers<void>(); + const server = Deno.serve({ + handler: (request) => { + const { + response, + socket, + } = Deno.upgradeWebSocket(request); + socket.onerror = (e) => { + console.error(e); + fail(); + }; + socket.onmessage = (_m) => { + socket.send(request.url.toString()); + socket.close(1001); + }; + socket.onclose = () => done.resolve(); + return response; + }, + port: servePort, + signal: ac.signal, + onListen: onListen(listeningDeferred.resolve), + onError: createOnErrorCb(ac), + }); + + await listeningDeferred.promise; + const def = Promise.withResolvers<void>(); + const ws = new WebSocket(`ws://localhost:${servePort}`); + ws.onmessage = (m) => + assertEquals(m.data, `http://localhost:${servePort}/`); + ws.onerror = (e) => { + console.error(e); + fail(); + }; + ws.onclose = () => def.resolve(); + ws.onopen = () => ws.send("foo"); + + await def.promise; + await done.promise; + ac.abort(); + await server.finished; + }, +); + +Deno.test( + { permissions: { net: true } }, + async function httpVeryLargeRequest() { + const deferred = Promise.withResolvers<void>(); + const listeningDeferred = Promise.withResolvers<void>(); + const ac = new AbortController(); + + let headers: Headers; + const server = Deno.serve({ + handler: (request) => { + headers = request.headers; + deferred.resolve(); + return new Response(""); + }, + port: servePort, + signal: ac.signal, + onListen: onListen(listeningDeferred.resolve), + onError: createOnErrorCb(ac), + }); + + await listeningDeferred.promise; + const conn = await Deno.connect({ port: servePort }); + // Send GET request with a body + content-length. + const encoder = new TextEncoder(); + const smthElse = "x".repeat(16 * 1024 + 256); + const body = + `GET / HTTP/1.1\r\nHost: 127.0.0.1:2333\r\nContent-Length: 5\r\nSomething-Else: ${smthElse}\r\n\r\n`; + const writeResult = await conn.write(encoder.encode(body)); + assertEquals(body.length, writeResult); + await deferred.promise; + conn.close(); + assertEquals(headers!.get("content-length"), "5"); + assertEquals(headers!.get("something-else"), smthElse); + ac.abort(); + await server.finished; + }, +); + +Deno.test( + { permissions: { net: true } }, + async function httpVeryLargeRequestAndBody() { + const deferred = Promise.withResolvers<void>(); + const listeningDeferred = Promise.withResolvers<void>(); + const ac = new AbortController(); + + let headers: Headers; + let text: string; + const server = Deno.serve({ + handler: async (request) => { + headers = request.headers; + text = await request.text(); + deferred.resolve(); + return new Response(""); + }, + port: servePort, + signal: ac.signal, + onListen: onListen(listeningDeferred.resolve), + onError: createOnErrorCb(ac), + }); + + await listeningDeferred.promise; + const conn = await Deno.connect({ port: servePort }); + // Send GET request with a body + content-length. + const encoder = new TextEncoder(); + const smthElse = "x".repeat(16 * 1024 + 256); + const reqBody = "hello world".repeat(1024); + let body = + `PUT / HTTP/1.1\r\nHost: 127.0.0.1:2333\r\nContent-Length: ${reqBody.length}\r\nSomething-Else: ${smthElse}\r\n\r\n${reqBody}`; + + while (body.length > 0) { + const writeResult = await conn.write(encoder.encode(body)); + body = body.slice(writeResult); + } + + await deferred.promise; + conn.close(); + + assertEquals(headers!.get("content-length"), `${reqBody.length}`); + assertEquals(headers!.get("something-else"), smthElse); + assertEquals(text!, reqBody); + ac.abort(); + await server.finished; + }, +); + +Deno.test( + { permissions: { net: true } }, + async function httpConnectionClose() { + const deferred = Promise.withResolvers<void>(); + const ac = new AbortController(); + const listeningDeferred = Promise.withResolvers<void>(); + + const server = Deno.serve({ + handler: () => { + deferred.resolve(); + return new Response(""); + }, + port: servePort, + signal: ac.signal, + onListen: onListen(listeningDeferred.resolve), + onError: createOnErrorCb(ac), + }); + + await listeningDeferred.promise; + const conn = await Deno.connect({ port: servePort }); + // Send GET request with a body + connection: close. + const encoder = new TextEncoder(); + const body = + `GET / HTTP/1.1\r\nHost: 127.0.0.1:2333\r\nConnection: Close\r\n\r\n`; + const writeResult = await conn.write(encoder.encode(body)); + assertEquals(body.length, writeResult); + + await deferred.promise; + conn.close(); + + ac.abort(); + await server.finished; + }, +); + +async function testDuplex( + reader: ReadableStreamDefaultReader<Uint8Array>, + writable: WritableStreamDefaultWriter<Uint8Array>, +) { + 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); +} + +Deno.test( + { permissions: { net: true } }, + async function httpServerStreamDuplexDirect() { + const { promise, resolve } = Promise.withResolvers<void>(); + const ac = new AbortController(); + + const server = Deno.serve( + { port: servePort, signal: ac.signal }, + (request: Request) => { + assert(request.body); + resolve(); + return new Response(request.body); + }, + ); + + const { readable, writable } = new TransformStream(); + const resp = await fetch(`http://127.0.0.1:${servePort}/`, { + method: "POST", + body: readable, + }); + + await promise; + assert(resp.body); + await testDuplex(resp.body.getReader(), writable.getWriter()); + ac.abort(); + await server.finished; + }, +); + +// Test that a duplex stream passing through JavaScript also works (ie: that the request body resource +// is still alive). https://github.com/denoland/deno/pull/20206 +Deno.test( + { permissions: { net: true } }, + async function httpServerStreamDuplexJavascript() { + const { promise, resolve } = Promise.withResolvers<void>(); + const ac = new AbortController(); + + const server = Deno.serve( + { port: servePort, signal: ac.signal }, + (request: Request) => { + assert(request.body); + resolve(); + const reader = request.body.getReader(); + return new Response( + new ReadableStream({ + async pull(controller) { + await new Promise((r) => setTimeout(r, 100)); + const { done, value } = await reader.read(); + if (done) { + controller.close(); + } else { + controller.enqueue(value); + } + }, + }), + ); + }, + ); + + const { readable, writable } = new TransformStream(); + const resp = await fetch(`http://127.0.0.1:${servePort}/`, { + method: "POST", + body: readable, + }); + + await promise; + assert(resp.body); + await testDuplex(resp.body.getReader(), writable.getWriter()); + ac.abort(); + await server.finished; + }, +); + +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. + const listeningDeferred = Promise.withResolvers<void>(); + const deferred = Promise.withResolvers<void>(); + const ac = new AbortController(); + + let counter = 0; + + const deferreds = [ + Promise.withResolvers<void>(), + Promise.withResolvers<void>(), + Promise.withResolvers<void>(), + ]; + + 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:${servePort}\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; + + try { + 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()); + } catch (e) { + console.error(e); + } + } + + 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()); + } + + const server = Deno.serve({ + handler: () => { + deferred.resolve(); + return new Response(periodicStream()); + }, + port: servePort, + signal: ac.signal, + onListen: onListen(listeningDeferred.resolve), + onError: createOnErrorCb(ac), + }); + + await listeningDeferred.promise; + // start a client + const clientConn = await Deno.connect({ port: servePort }); + + const r1 = await writeRequest(clientConn); + assertEquals(r1, "0\n1\n2\n"); + + ac.abort(); + await deferred.promise; + await server.finished; + clientConn.close(); + }, +); + +// Make sure that the chunks of a large response aren't repeated or corrupted in some other way by +// scatterning sentinels throughout. +// https://github.com/denoland/fresh/issues/1699 +Deno.test( + { permissions: { net: true } }, + async function httpLargeReadableStreamChunk() { + const ac = new AbortController(); + const server = Deno.serve({ + handler() { + return new Response( + new ReadableStream({ + start(controller) { + const buffer = new Uint8Array(1024 * 1024); + // Mark the buffer with sentinels + for (let i = 0; i < 256; i++) { + buffer[i * 4096] = i; + } + controller.enqueue(buffer); + controller.close(); + }, + }), + ); + }, + port: servePort, + signal: ac.signal, + }); + const response = await fetch(`http://localhost:${servePort}/`); + const body = await response.arrayBuffer(); + assertEquals(1024 * 1024, body.byteLength); + const buffer = new Uint8Array(body); + for (let i = 0; i < 256; i++) { + assertEquals( + i, + buffer[i * 4096], + `sentinel mismatch at index ${i * 4096}`, + ); + } + ac.abort(); + await server.finished; + }, +); + +Deno.test( + { permissions: { net: true } }, + async function httpRequestLatin1Headers() { + const listeningDeferred = Promise.withResolvers<void>(); + const deferred = Promise.withResolvers<void>(); + const ac = new AbortController(); + const server = Deno.serve({ + handler: (request) => { + assertEquals(request.headers.get("X-Header-Test"), "á"); + deferred.resolve(); + return new Response("hello", { headers: { "X-Header-Test": "Æ" } }); + }, + port: servePort, + signal: ac.signal, + onListen: onListen(listeningDeferred.resolve), + onError: createOnErrorCb(ac), + }); + + await listeningDeferred.promise; + const clientConn = await Deno.connect({ port: servePort }); + const requestText = + `GET / HTTP/1.1\r\nHost: 127.0.0.1:${servePort}\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)); + } + + const buf = new Uint8Array(1024); + await clientConn.read(buf); + + await deferred.promise; + const responseText = new TextDecoder("iso-8859-1").decode(buf); + clientConn.close(); + + ac.abort(); + await server.finished; + + assertMatch(responseText, /\r\n[Xx]-[Hh]eader-[Tt]est: Æ\r\n/); + }, +); + +Deno.test( + { permissions: { net: true } }, + async function httpServerRequestWithoutPath() { + const deferred = Promise.withResolvers<void>(); + const listeningDeferred = Promise.withResolvers<void>(); + const ac = new AbortController(); + + const server = Deno.serve({ + handler: async (request) => { + // FIXME: + // assertEquals(new URL(request.url).href, `http://127.0.0.1:${servePort}/`); + assertEquals(await request.text(), ""); + deferred.resolve(); + return new Response("11"); + }, + port: servePort, + signal: ac.signal, + onListen: onListen(listeningDeferred.resolve), + onError: createOnErrorCb(ac), + }); + + await listeningDeferred.promise; + const clientConn = await Deno.connect({ port: servePort }); + + 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:${servePort} HTTP/1.1\r\nHost: 127.0.0.1:${servePort}\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 deferred.promise; + + ac.abort(); + await server.finished; + }, +); + +Deno.test( + { permissions: { net: true } }, + async function httpCookieConcatenation() { + const deferred = Promise.withResolvers<void>(); + const listeningDeferred = Promise.withResolvers<void>(); + const ac = new AbortController(); + + const server = Deno.serve({ + handler: async (request) => { + assertEquals(await request.text(), ""); + assertEquals(request.headers.get("cookie"), "foo=bar; bar=foo"); + deferred.resolve(); + return new Response("ok"); + }, + port: servePort, + signal: ac.signal, + onListen: onListen(listeningDeferred.resolve), + onError: createOnErrorCb(ac), + reusePort: true, + }); + + await listeningDeferred.promise; + const resp = await fetch(`http://127.0.0.1:${servePort}/`, { + headers: [ + ["connection", "close"], + ["cookie", "foo=bar"], + ["cookie", "bar=foo"], + ], + }); + await deferred.promise; + + const text = await resp.text(); + assertEquals(text, "ok"); + + ac.abort(); + await server.finished; + }, +); + +// 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 deferred = Promise.withResolvers<void>(); + const listeningDeferred = Promise.withResolvers<void>(); + const ac = new AbortController(); + + const hostname = "localhost"; + + const server = Deno.serve({ + handler: () => { + deferred.resolve(); + return new Response("ok"); + }, + port: servePort, + signal: ac.signal, + onListen: onListen(listeningDeferred.resolve), + onError: createOnErrorCb(ac), + }); + + await listeningDeferred.promise; + const url = `http://${hostname}:${servePort}/`; + const args = ["-X", "DELETE", url]; + const { success } = await new Deno.Command("curl", { + args, + stdout: "null", + stderr: "null", + }).output(); + assert(success); + await deferred.promise; + ac.abort(); + + await server.finished; + }, +); + +// FIXME: +Deno.test( + { permissions: { net: true } }, + async function httpServerRespondNonAsciiUint8Array() { + const deferred = Promise.withResolvers<void>(); + const listeningDeferred = Promise.withResolvers<void>(); + const ac = new AbortController(); + + const server = Deno.serve({ + handler: (request) => { + assertEquals(request.body, null); + deferred.resolve(); + return new Response(new Uint8Array([128])); + }, + port: servePort, + signal: ac.signal, + onListen: onListen(listeningDeferred.resolve), + onError: createOnErrorCb(ac), + }); + await listeningDeferred.resolve; + const resp = await fetch(`http://localhost:${servePort}/`); + + await deferred.promise; + + assertEquals(resp.status, 200); + const body = await resp.arrayBuffer(); + assertEquals(new Uint8Array(body), new Uint8Array([128])); + + ac.abort(); + await server.finished; + }, +); + +// Some of these tests are ported from Hyper +// https://github.com/hyperium/hyper/blob/889fa2d87252108eb7668b8bf034ffcc30985117/src/proto/h1/role.rs +// https://github.com/hyperium/hyper/blob/889fa2d87252108eb7668b8bf034ffcc30985117/tests/server.rs + +Deno.test( + { permissions: { net: true } }, + async function httpServerParseRequest() { + const deferred = Promise.withResolvers<void>(); + const listeningDeferred = Promise.withResolvers<void>(); + const ac = new AbortController(); + + const server = Deno.serve({ + handler: (request) => { + assertEquals(request.method, "GET"); + assertEquals(request.headers.get("host"), "deno.land"); + deferred.resolve(); + return new Response("ok"); + }, + port: servePort, + signal: ac.signal, + onListen: onListen(listeningDeferred.resolve), + onError: createOnErrorCb(ac), + }); + + await listeningDeferred.promise; + const conn = await Deno.connect({ port: servePort }); + const encoder = new TextEncoder(); + const body = `GET /echo HTTP/1.1\r\nHost: deno.land\r\n\r\n`; + const writeResult = await conn.write(encoder.encode(body)); + assertEquals(body.length, writeResult); + await deferred.promise; + conn.close(); + + ac.abort(); + await server.finished; + }, +); + +Deno.test( + { permissions: { net: true } }, + async function httpServerParseHeaderHtabs() { + const deferred = Promise.withResolvers<void>(); + const listeningDeferred = Promise.withResolvers<void>(); + const ac = new AbortController(); + + const server = Deno.serve({ + handler: (request) => { + assertEquals(request.method, "GET"); + assertEquals(request.headers.get("server"), "hello\tworld"); + deferred.resolve(); + return new Response("ok"); + }, + port: servePort, + signal: ac.signal, + onListen: onListen(listeningDeferred.resolve), + onError: createOnErrorCb(ac), + }); + + await listeningDeferred.promise; + const conn = await Deno.connect({ port: servePort }); + const encoder = new TextEncoder(); + const body = `GET / HTTP/1.1\r\nserver: hello\tworld\r\n\r\n`; + const writeResult = await conn.write(encoder.encode(body)); + assertEquals(body.length, writeResult); + await deferred.promise; + conn.close(); + + ac.abort(); + await server.finished; + }, +); + +Deno.test( + { permissions: { net: true } }, + async function httpServerGetShouldIgnoreBody() { + const deferred = Promise.withResolvers<void>(); + const listeningDeferred = Promise.withResolvers<void>(); + const ac = new AbortController(); + + const server = Deno.serve({ + handler: async (request) => { + assertEquals(request.method, "GET"); + assertEquals(await request.text(), ""); + deferred.resolve(); + return new Response("ok"); + }, + port: servePort, + signal: ac.signal, + onListen: onListen(listeningDeferred.resolve), + onError: createOnErrorCb(ac), + }); + + await listeningDeferred.promise; + const conn = await Deno.connect({ port: servePort }); + const encoder = new TextEncoder(); + // Connection: close = don't try to parse the body as a new request + const body = + `GET / HTTP/1.1\r\nHost: example.domain\r\nConnection: close\r\n\r\nI shouldn't be read.\r\n`; + const writeResult = await conn.write(encoder.encode(body)); + assertEquals(body.length, writeResult); + await deferred.promise; + conn.close(); + + ac.abort(); + await server.finished; + }, +); + +Deno.test( + { permissions: { net: true } }, + async function httpServerPostWithBody() { + const deferred = Promise.withResolvers<void>(); + const listeningDeferred = Promise.withResolvers<void>(); + const ac = new AbortController(); + + const server = Deno.serve({ + handler: async (request) => { + assertEquals(request.method, "POST"); + assertEquals(await request.text(), "I'm a good request."); + deferred.resolve(); + return new Response("ok"); + }, + port: servePort, + signal: ac.signal, + onListen: onListen(listeningDeferred.resolve), + onError: createOnErrorCb(ac), + }); + + await listeningDeferred.promise; + const conn = await Deno.connect({ port: servePort }); + const encoder = new TextEncoder(); + const body = + `POST / HTTP/1.1\r\nHost: example.domain\r\nContent-Length: 19\r\n\r\nI'm a good request.`; + const writeResult = await conn.write(encoder.encode(body)); + assertEquals(body.length, writeResult); + await deferred.promise; + conn.close(); + + ac.abort(); + await server.finished; + }, +); + +type TestCase = { + headers?: Record<string, string>; + // deno-lint-ignore no-explicit-any + body: any; + expectsChunked?: boolean; + expectsConnLen?: boolean; +}; + +function hasHeader(msg: string, name: string): boolean { + const n = msg.indexOf("\r\n\r\n") || msg.length; + return msg.slice(0, n).includes(name); +} + +function createServerLengthTest(name: string, testCase: TestCase) { + Deno.test(name, async function () { + const deferred = Promise.withResolvers<void>(); + const listeningDeferred = Promise.withResolvers<void>(); + const ac = new AbortController(); + + const server = Deno.serve({ + handler: (request) => { + assertEquals(request.method, "GET"); + deferred.resolve(); + return new Response(testCase.body, testCase.headers ?? {}); + }, + port: servePort, + signal: ac.signal, + onListen: onListen(listeningDeferred.resolve), + onError: createOnErrorCb(ac), + }); + + await listeningDeferred.promise; + const conn = await Deno.connect({ port: servePort }); + const encoder = new TextEncoder(); + const body = + `GET / HTTP/1.1\r\nHost: example.domain\r\nConnection: close\r\n\r\n`; + const writeResult = await conn.write(encoder.encode(body)); + assertEquals(body.length, writeResult); + await deferred.promise; + + const decoder = new TextDecoder(); + let msg = ""; + while (true) { + const buf = new Uint8Array(1024); + const readResult = await conn.read(buf); + if (!readResult) { + break; + } + msg += decoder.decode(buf.subarray(0, readResult)); + try { + assert( + testCase.expectsChunked == hasHeader(msg, "Transfer-Encoding:"), + ); + assert(testCase.expectsChunked == hasHeader(msg, "chunked")); + assert(testCase.expectsConnLen == hasHeader(msg, "Content-Length:")); + + const n = msg.indexOf("\r\n\r\n") + 4; + + if (testCase.expectsChunked) { + assertEquals(msg.slice(n + 1, n + 3), "\r\n"); + assertEquals(msg.slice(msg.length - 7), "\r\n0\r\n\r\n"); + } + + if (testCase.expectsConnLen && typeof testCase.body === "string") { + assertEquals(msg.slice(n), testCase.body); + } + break; + } catch { + continue; + } + } + + conn.close(); + + ac.abort(); + await server.finished; + }); +} + +// Quick and dirty way to make a readable stream from a string. Alternatively, +// `readableStreamFromReader(file)` could be used. +function stream(s: string): ReadableStream<Uint8Array> { + return new Response(s).body!; +} + +createServerLengthTest("fixedResponseKnown", { + headers: { "content-length": "11" }, + body: "foo bar baz", + expectsChunked: false, + expectsConnLen: true, +}); + +createServerLengthTest("fixedResponseUnknown", { + headers: { "content-length": "11" }, + body: stream("foo bar baz"), + expectsChunked: true, + expectsConnLen: false, +}); + +createServerLengthTest("fixedResponseKnownEmpty", { + headers: { "content-length": "0" }, + body: "", + expectsChunked: false, + expectsConnLen: true, +}); + +createServerLengthTest("chunkedRespondKnown", { + headers: { "transfer-encoding": "chunked" }, + body: "foo bar baz", + expectsChunked: false, + expectsConnLen: true, +}); + +createServerLengthTest("chunkedRespondUnknown", { + headers: { "transfer-encoding": "chunked" }, + body: stream("foo bar baz"), + expectsChunked: true, + expectsConnLen: false, +}); + +createServerLengthTest("autoResponseWithKnownLength", { + body: "foo bar baz", + expectsChunked: false, + expectsConnLen: true, +}); + +createServerLengthTest("autoResponseWithUnknownLength", { + body: stream("foo bar baz"), + expectsChunked: true, + expectsConnLen: false, +}); + +createServerLengthTest("autoResponseWithKnownLengthEmpty", { + body: "", + expectsChunked: false, + expectsConnLen: true, +}); + +createServerLengthTest("autoResponseWithUnknownLengthEmpty", { + body: stream(""), + expectsChunked: true, + expectsConnLen: false, +}); + +Deno.test( + { permissions: { net: true } }, + async function httpServerPostWithContentLengthBody() { + const deferred = Promise.withResolvers<void>(); + const listeningDeferred = Promise.withResolvers<void>(); + const ac = new AbortController(); + + const server = Deno.serve({ + handler: async (request) => { + assertEquals(request.method, "POST"); + assertEquals(request.headers.get("content-length"), "5"); + assertEquals(await request.text(), "hello"); + deferred.resolve(); + return new Response("ok"); + }, + port: servePort, + signal: ac.signal, + onListen: onListen(listeningDeferred.resolve), + onError: createOnErrorCb(ac), + }); + + await listeningDeferred.promise; + const conn = await Deno.connect({ port: servePort }); + const encoder = new TextEncoder(); + + const body = + `POST / HTTP/1.1\r\nHost: example.domain\r\nContent-Length: 5\r\n\r\nhello`; + const writeResult = await conn.write(encoder.encode(body)); + assertEquals(body.length, writeResult); + await deferred.promise; + + conn.close(); + + ac.abort(); + await server.finished; + }, +); + +Deno.test( + { permissions: { net: true } }, + async function httpServerPostWithInvalidPrefixContentLength() { + const ac = new AbortController(); + const { promise, resolve } = Promise.withResolvers<void>(); + const server = Deno.serve({ + handler: () => { + throw new Error("unreachable"); + }, + port: servePort, + signal: ac.signal, + onListen: onListen(resolve), + onError: createOnErrorCb(ac), + }); + + await promise; + const conn = await Deno.connect({ port: servePort }); + const encoder = new TextEncoder(); + const decoder = new TextDecoder(); + + const body = + `POST / HTTP/1.1\r\nHost: example.domain\r\nContent-Length: +5\r\n\r\nhello`; + const writeResult = await conn.write(encoder.encode(body)); + assertEquals(body.length, writeResult); + + const buf = new Uint8Array(1024); + const readResult = await conn.read(buf); + assert(readResult); + const msg = decoder.decode(buf.subarray(0, readResult)); + assert(msg.includes("HTTP/1.1 400 Bad Request")); + + conn.close(); + + ac.abort(); + await server.finished; + }, +); + +Deno.test( + { permissions: { net: true } }, + async function httpServerPostWithChunkedBody() { + const deferred = Promise.withResolvers<void>(); + const listeningDeferred = Promise.withResolvers<void>(); + const ac = new AbortController(); + + const server = Deno.serve({ + handler: async (request) => { + assertEquals(request.method, "POST"); + assertEquals(await request.text(), "qwert"); + deferred.resolve(); + return new Response("ok"); + }, + port: servePort, + signal: ac.signal, + onListen: onListen(listeningDeferred.resolve), + onError: createOnErrorCb(ac), + }); + + await listeningDeferred.promise; + const conn = await Deno.connect({ port: servePort }); + const encoder = new TextEncoder(); + + const body = + `POST / HTTP/1.1\r\nHost: example.domain\r\nTransfer-Encoding: chunked\r\n\r\n1\r\nq\r\n2\r\nwe\r\n2\r\nrt\r\n0\r\n\r\n`; + const writeResult = await conn.write(encoder.encode(body)); + assertEquals(body.length, writeResult); + await deferred.promise; + + conn.close(); + + ac.abort(); + await server.finished; + }, +); + +Deno.test( + { permissions: { net: true } }, + async function httpServerPostWithIncompleteBody() { + const deferred = Promise.withResolvers<void>(); + const listeningDeferred = Promise.withResolvers<void>(); + const ac = new AbortController(); + + const server = Deno.serve({ + handler: async (r) => { + deferred.resolve(); + assertEquals(await r.text(), "12345"); + return new Response("ok"); + }, + port: servePort, + signal: ac.signal, + onListen: onListen(listeningDeferred.resolve), + onError: createOnErrorCb(ac), + }); + + await listeningDeferred.promise; + const conn = await Deno.connect({ port: servePort }); + const encoder = new TextEncoder(); + + const body = + `POST / HTTP/1.1\r\nHost: example.domain\r\nContent-Length: 10\r\n\r\n12345`; + const writeResult = await conn.write(encoder.encode(body)); + assertEquals(body.length, writeResult); + + await deferred.promise; + conn.close(); + + ac.abort(); + await server.finished; + }, +); + +Deno.test( + { permissions: { net: true } }, + async function httpServerHeadResponseDoesntSendBody() { + const deferred = Promise.withResolvers<void>(); + const listeningDeferred = Promise.withResolvers<void>(); + const ac = new AbortController(); + + const server = Deno.serve({ + handler: () => { + deferred.resolve(); + return new Response("NaN".repeat(100)); + }, + port: servePort, + signal: ac.signal, + onListen: onListen(listeningDeferred.resolve), + onError: createOnErrorCb(ac), + }); + + await listeningDeferred.promise; + const conn = await Deno.connect({ port: servePort }); + const encoder = new TextEncoder(); + const decoder = new TextDecoder(); + + const body = + `HEAD / HTTP/1.1\r\nHost: example.domain\r\nConnection: close\r\n\r\n`; + const writeResult = await conn.write(encoder.encode(body)); + assertEquals(body.length, writeResult); + + await deferred.promise; + + const buf = new Uint8Array(1024); + const readResult = await conn.read(buf); + assert(readResult); + const msg = decoder.decode(buf.subarray(0, readResult)); + + assert(msg.includes("content-length: 300\r\n")); + + conn.close(); + + ac.abort(); + await server.finished; + }, +); + +function makeTempData(size: number) { + return new Uint8Array(size).fill(1); +} + +async function makeTempFile(size: number) { + const tmpFile = await Deno.makeTempFile(); + using file = await Deno.open(tmpFile, { write: true, read: true }); + const data = makeTempData(size); + await file.write(data); + + return await Deno.open(tmpFile, { write: true, read: true }); +} + +const compressionTestCases = [ + { name: "Empty", length: 0, in: {}, out: {}, expect: null }, + { + name: "EmptyAcceptGzip", + length: 0, + in: { "Accept-Encoding": "gzip" }, + out: {}, + expect: null, + }, + // This technically would be compressible if not for the size, however the size_hint is not implemented + // for FileResource and we don't currently peek ahead on resources. + // { + // name: "EmptyAcceptGzip2", + // length: 0, + // in: { "Accept-Encoding": "gzip" }, + // out: { "Content-Type": "text/plain" }, + // expect: null, + // }, + { name: "Incompressible", length: 1024, in: {}, out: {}, expect: null }, + { + name: "IncompressibleAcceptGzip", + length: 1024, + in: { "Accept-Encoding": "gzip" }, + out: {}, + expect: null, + }, + { + name: "IncompressibleType", + length: 1024, + in: { "Accept-Encoding": "gzip" }, + out: { "Content-Type": "text/fake" }, + expect: null, + }, + { + name: "CompressibleType", + length: 1024, + in: { "Accept-Encoding": "gzip" }, + out: { "Content-Type": "text/plain" }, + expect: "gzip", + }, + { + name: "CompressibleType2", + length: 1024, + in: { "Accept-Encoding": "gzip, deflate, br" }, + out: { "Content-Type": "text/plain" }, + expect: "gzip", + }, + { + name: "CompressibleType3", + length: 1024, + in: { "Accept-Encoding": "br" }, + out: { "Content-Type": "text/plain" }, + expect: "br", + }, + { + name: "IncompressibleRange", + length: 1024, + in: { "Accept-Encoding": "gzip" }, + out: { "Content-Type": "text/plain", "Content-Range": "1" }, + expect: null, + }, + { + name: "IncompressibleCE", + length: 1024, + in: { "Accept-Encoding": "gzip" }, + out: { "Content-Type": "text/plain", "Content-Encoding": "random" }, + expect: null, + }, + { + name: "IncompressibleCC", + length: 1024, + in: { "Accept-Encoding": "gzip" }, + out: { "Content-Type": "text/plain", "Cache-Control": "no-transform" }, + expect: null, + }, + { + name: "BadHeader", + length: 1024, + in: { "Accept-Encoding": "\x81" }, + out: { "Content-Type": "text/plain", "Cache-Control": "no-transform" }, + expect: null, + }, +]; + +for (const testCase of compressionTestCases) { + const name = `httpServerCompression${testCase.name}`; + Deno.test( + { permissions: { net: true, write: true, read: true } }, + { + [name]: async function () { + const deferred = Promise.withResolvers<void>(); + const listeningDeferred = Promise.withResolvers<void>(); + const ac = new AbortController(); + const server = Deno.serve({ + handler: async (_request) => { + const f = await makeTempFile(testCase.length); + deferred.resolve(); + // deno-lint-ignore no-explicit-any + const headers = testCase.out as any; + headers["Content-Length"] = testCase.length.toString(); + return new Response(f.readable, { + headers: headers as HeadersInit, + }); + }, + port: servePort, + signal: ac.signal, + onListen: onListen(listeningDeferred.resolve), + onError: createOnErrorCb(ac), + }); + try { + await listeningDeferred.promise; + const resp = await fetch(`http://127.0.0.1:${servePort}/`, { + headers: testCase.in as HeadersInit, + }); + await deferred.promise; + const body = await resp.arrayBuffer(); + if (testCase.expect == null) { + assertEquals(body.byteLength, testCase.length); + assertEquals( + resp.headers.get("content-length"), + testCase.length.toString(), + ); + assertEquals( + resp.headers.get("content-encoding"), + testCase.out["Content-Encoding"] || null, + ); + } else if (testCase.expect == "gzip") { + // Note the fetch will transparently decompress this response, BUT we can detect that a response + // was compressed by the lack of a content length. + assertEquals(body.byteLength, testCase.length); + assertEquals(resp.headers.get("content-encoding"), null); + assertEquals(resp.headers.get("content-length"), null); + } + } finally { + ac.abort(); + await server.finished; + } + }, + }[name], + ); +} + +Deno.test( + { permissions: { net: true, write: true, read: true } }, + async function httpServerPostFile() { + const deferred = Promise.withResolvers<void>(); + const listeningDeferred = Promise.withResolvers<void>(); + const ac = new AbortController(); + + const server = Deno.serve({ + handler: async (request) => { + assertEquals( + new Uint8Array(await request.arrayBuffer()), + makeTempData(70 * 1024), + ); + deferred.resolve(); + return new Response("ok"); + }, + port: servePort, + signal: ac.signal, + onListen: onListen(listeningDeferred.resolve), + onError: createOnErrorCb(ac), + }); + + await listeningDeferred.promise; + const f = await makeTempFile(70 * 1024); + const response = await fetch(`http://localhost:${servePort}/`, { + method: "POST", + body: f.readable, + }); + + await deferred.promise; + + assertEquals(response.status, 200); + assertEquals(await response.text(), "ok"); + + ac.abort(); + await server.finished; + }, +); + +for (const delay of ["delay", "nodelay"]) { + for (const url of ["text", "file", "stream"]) { + // Ensure that we don't panic when the incoming TCP request was dropped + // https://github.com/denoland/deno/issues/20315 and that we correctly + // close/cancel the response + Deno.test({ + permissions: { read: true, write: true, net: true }, + name: `httpServerTcpCancellation_${url}_${delay}`, + fn: async function () { + const ac = new AbortController(); + const streamCancelled = url == "stream" + ? Promise.withResolvers<void>() + : undefined; + const listeningDeferred = Promise.withResolvers<void>(); + const waitForAbort = Promise.withResolvers<void>(); + const waitForRequest = Promise.withResolvers<void>(); + const server = Deno.serve({ + port: servePort, + signal: ac.signal, + onListen: onListen(listeningDeferred.resolve), + handler: async (req: Request) => { + let respBody = null; + if (req.url.includes("/text")) { + respBody = "text"; + } else if (req.url.includes("/file")) { + respBody = (await makeTempFile(1024)).readable; + } else if (req.url.includes("/stream")) { + respBody = new ReadableStream({ + start(controller) { + controller.enqueue(new Uint8Array([1])); + }, + cancel(reason) { + streamCancelled!.resolve(reason); + }, + }); + } else { + fail(); + } + waitForRequest.resolve(); + await waitForAbort.promise; + + if (delay == "delay") { + await new Promise((r) => setTimeout(r, 1000)); + } + // Allocate the request body + req.body; + return new Response(respBody); + }, + }); + + await listeningDeferred.promise; + + // Create a POST request and drop it once the server has received it + const conn = await Deno.connect({ port: servePort }); + const writer = conn.writable.getWriter(); + await writer.write( + new TextEncoder().encode(`POST /${url} HTTP/1.0\n\n`), + ); + await waitForRequest.promise; + await writer.close(); + + waitForAbort.resolve(); + + // Wait for cancellation before we shut the server down + if (streamCancelled !== undefined) { + await streamCancelled; + } + + // Since the handler has a chance of creating resources or running async + // ops, we need to use a graceful shutdown here to ensure they have fully + // drained. + await server.shutdown(); + + await server.finished; + }, + }); + } +} + +Deno.test( + { permissions: { net: true } }, + async function httpServerCancelFetch() { + const request2 = Promise.withResolvers<void>(); + const request2Aborted = Promise.withResolvers<string>(); + const { finished, abort } = await makeServer(async (req) => { + if (req.url.endsWith("/1")) { + const fetchRecursive = await fetch(`http://localhost:${servePort}/2`); + return new Response(fetchRecursive.body); + } else if (req.url.endsWith("/2")) { + request2.resolve(); + return new Response( + new ReadableStream({ + start(_controller) {/* just hang */}, + cancel(reason) { + request2Aborted.resolve(reason); + }, + }), + ); + } + fail(); + }); + const fetchAbort = new AbortController(); + const fetchPromise = await fetch(`http://localhost:${servePort}/1`, { + signal: fetchAbort.signal, + }); + await fetchPromise; + await request2.promise; + fetchAbort.abort(); + assertEquals("resource closed", await request2Aborted.promise); + + abort(); + await finished; + }, +); + +Deno.test( + { permissions: { read: true, net: true } }, + async function httpServerWithTls() { + const ac = new AbortController(); + const { promise, resolve } = Promise.withResolvers<void>(); + const hostname = "127.0.0.1"; + + const server = Deno.serve({ + handler: () => new Response("Hello World"), + hostname, + port: servePort, + signal: ac.signal, + onListen: onListen(resolve), + onError: createOnErrorCb(ac), + cert: Deno.readTextFileSync("tests/testdata/tls/localhost.crt"), + key: Deno.readTextFileSync("tests/testdata/tls/localhost.key"), + }); + + await promise; + const caCert = Deno.readTextFileSync("tests/testdata/tls/RootCA.pem"); + const client = Deno.createHttpClient({ caCerts: [caCert] }); + const resp = await fetch(`https://localhost:${servePort}/`, { + client, + headers: { "connection": "close" }, + }); + + const respBody = await resp.text(); + assertEquals("Hello World", respBody); + + client.close(); + ac.abort(); + await server.finished; + }, +); + +Deno.test( + { permissions: { net: true, write: true, read: true } }, + async function httpServerRequestCLTE() { + const ac = new AbortController(); + const listeningDeferred = Promise.withResolvers<void>(); + const deferred = Promise.withResolvers<void>(); + + const server = Deno.serve({ + handler: async (req) => { + assertEquals(await req.text(), ""); + deferred.resolve(); + return new Response("ok"); + }, + port: servePort, + signal: ac.signal, + onListen: onListen(listeningDeferred.resolve), + onError: createOnErrorCb(ac), + }); + + await listeningDeferred.promise; + const conn = await Deno.connect({ port: servePort }); + const encoder = new TextEncoder(); + + const body = + `POST / HTTP/1.1\r\nHost: example.domain\r\nContent-Length: 13\r\nTransfer-Encoding: chunked\r\n\r\n0\r\n\r\nEXTRA`; + const writeResult = await conn.write(encoder.encode(body)); + assertEquals(body.length, writeResult); + await deferred.promise; + + conn.close(); + + ac.abort(); + await server.finished; + }, +); + +Deno.test( + { permissions: { net: true, write: true, read: true } }, + async function httpServerRequestTETE() { + const ac = new AbortController(); + const { promise, resolve } = Promise.withResolvers<void>(); + + const server = Deno.serve({ + handler: () => { + throw new Error("oops"); + }, + port: servePort, + signal: ac.signal, + onListen: onListen(resolve), + onError: createOnErrorCb(ac), + }); + + const encoder = new TextEncoder(); + const decoder = new TextDecoder(); + + const variations = [ + "Transfer-Encoding : chunked", + "Transfer-Encoding: xchunked", + "Transfer-Encoding: chunkedx", + "Transfer-Encoding\n: chunked", + ]; + + await promise; + for (const teHeader of variations) { + const conn = await Deno.connect({ port: servePort }); + const body = + `POST / HTTP/1.1\r\nHost: example.domain\r\n${teHeader}\r\n\r\n0\r\n\r\n`; + const writeResult = await conn.write(encoder.encode(body)); + assertEquals(body.length, writeResult); + + const buf = new Uint8Array(1024); + const readResult = await conn.read(buf); + assert(readResult); + const msg = decoder.decode(buf.subarray(0, readResult)); + assert(msg.includes("HTTP/1.1 400 Bad Request\r\n")); + + conn.close(); + } + + ac.abort(); + await server.finished; + }, +); + +Deno.test( + { permissions: { net: true } }, + async function httpServer204ResponseDoesntSendContentLength() { + const { promise, resolve } = Promise.withResolvers<void>(); + const ac = new AbortController(); + const server = Deno.serve({ + handler: (_request) => new Response(null, { status: 204 }), + port: servePort, + signal: ac.signal, + onListen: onListen(resolve), + onError: createOnErrorCb(ac), + }); + + try { + await promise; + const resp = await fetch(`http://127.0.0.1:${servePort}/`, { + method: "GET", + headers: { "connection": "close" }, + }); + assertEquals(resp.status, 204); + assertEquals(resp.headers.get("Content-Length"), null); + } finally { + ac.abort(); + await server.finished; + } + }, +); + +Deno.test( + { permissions: { net: true } }, + async function httpServer304ResponseDoesntSendBody() { + const deferred = Promise.withResolvers<void>(); + const ac = new AbortController(); + const listeningDeferred = Promise.withResolvers<void>(); + + const server = Deno.serve({ + handler: () => { + deferred.resolve(); + return new Response(null, { status: 304 }); + }, + port: servePort, + signal: ac.signal, + onListen: onListen(listeningDeferred.resolve), + onError: createOnErrorCb(ac), + }); + + await listeningDeferred.promise; + const conn = await Deno.connect({ port: servePort }); + const encoder = new TextEncoder(); + const decoder = new TextDecoder(); + + const body = + `GET / HTTP/1.1\r\nHost: example.domain\r\nConnection: close\r\n\r\n`; + const writeResult = await conn.write(encoder.encode(body)); + assertEquals(body.length, writeResult); + + await deferred.promise; + + const buf = new Uint8Array(1024); + const readResult = await conn.read(buf); + assert(readResult); + const msg = decoder.decode(buf.subarray(0, readResult)); + + assert(msg.startsWith("HTTP/1.1 304 Not Modified")); + assert(msg.endsWith("\r\n\r\n")); + + conn.close(); + + ac.abort(); + await server.finished; + }, +); + +Deno.test( + { permissions: { net: true } }, + async function httpServerExpectContinue() { + const deferred = Promise.withResolvers<void>(); + const ac = new AbortController(); + const listeningDeferred = Promise.withResolvers<void>(); + + const server = Deno.serve({ + handler: async (req) => { + deferred.resolve(); + assertEquals(await req.text(), "hello"); + return new Response(null, { status: 304 }); + }, + port: servePort, + signal: ac.signal, + onListen: onListen(listeningDeferred.resolve), + onError: createOnErrorCb(ac), + }); + + await listeningDeferred.promise; + const conn = await Deno.connect({ port: servePort }); + const encoder = new TextEncoder(); + const decoder = new TextDecoder(); + + { + const body = + `POST / HTTP/1.1\r\nHost: example.domain\r\nExpect: 100-continue\r\nContent-Length: 5\r\nConnection: close\r\n\r\n`; + const writeResult = await conn.write(encoder.encode(body)); + assertEquals(body.length, writeResult); + } + + await deferred.promise; + + { + const msgExpected = "HTTP/1.1 100 Continue\r\n\r\n"; + const buf = new Uint8Array(encoder.encode(msgExpected).byteLength); + const readResult = await conn.read(buf); + assert(readResult); + const msg = decoder.decode(buf.subarray(0, readResult)); + assert(msg.startsWith(msgExpected)); + } + + { + const body = "hello"; + const writeResult = await conn.write(encoder.encode(body)); + assertEquals(body.length, writeResult); + } + + const buf = new Uint8Array(1024); + const readResult = await conn.read(buf); + assert(readResult); + const msg = decoder.decode(buf.subarray(0, readResult)); + + assert(msg.startsWith("HTTP/1.1 304 Not Modified")); + conn.close(); + + ac.abort(); + await server.finished; + }, +); + +Deno.test( + { permissions: { net: true } }, + async function httpServerExpectContinueButNoBodyLOL() { + const deferred = Promise.withResolvers<void>(); + const listeningDeferred = Promise.withResolvers<void>(); + const ac = new AbortController(); + + const server = Deno.serve({ + handler: async (req) => { + deferred.resolve(); + assertEquals(await req.text(), ""); + return new Response(null, { status: 304 }); + }, + port: servePort, + signal: ac.signal, + onListen: onListen(listeningDeferred.resolve), + onError: createOnErrorCb(ac), + }); + + await listeningDeferred.promise; + const conn = await Deno.connect({ port: servePort }); + const encoder = new TextEncoder(); + const decoder = new TextDecoder(); + + { + // // no content-length or transfer-encoding means no body! + const body = + `POST / HTTP/1.1\r\nHost: example.domain\r\nExpect: 100-continue\r\nConnection: close\r\n\r\n`; + const writeResult = await conn.write(encoder.encode(body)); + assertEquals(body.length, writeResult); + } + + await deferred.promise; + + const buf = new Uint8Array(1024); + const readResult = await conn.read(buf); + assert(readResult); + const msg = decoder.decode(buf.subarray(0, readResult)); + + assert(msg.startsWith("HTTP/1.1 304 Not Modified")); + conn.close(); + + ac.abort(); + await server.finished; + }, +); + +const badRequests = [ + ["weirdMethodName", "GE T / HTTP/1.1\r\n\r\n"], + ["illegalRequestLength", "POST / HTTP/1.1\r\nContent-Length: foo\r\n\r\n"], + ["illegalRequestLength2", "POST / HTTP/1.1\r\nContent-Length: -1\r\n\r\n"], + ["illegalRequestLength3", "POST / HTTP/1.1\r\nContent-Length: 1.1\r\n\r\n"], + ["illegalRequestLength4", "POST / HTTP/1.1\r\nContent-Length: 1.\r\n\r\n"], +]; + +for (const [name, req] of badRequests) { + const testFn = { + [name]: async () => { + const ac = new AbortController(); + const { promise, resolve } = Promise.withResolvers<void>(); + + const server = Deno.serve({ + handler: () => { + throw new Error("oops"); + }, + port: servePort, + signal: ac.signal, + onListen: onListen(resolve), + onError: createOnErrorCb(ac), + }); + + await promise; + const conn = await Deno.connect({ port: servePort }); + const encoder = new TextEncoder(); + const decoder = new TextDecoder(); + + { + const writeResult = await conn.write(encoder.encode(req)); + assertEquals(req.length, writeResult); + } + + const buf = new Uint8Array(100); + const readResult = await conn.read(buf); + assert(readResult); + const msg = decoder.decode(buf.subarray(0, readResult)); + + assert(msg.startsWith("HTTP/1.1 400 ")); + conn.close(); + + ac.abort(); + await server.finished; + }, + }[name]; + + Deno.test( + { permissions: { net: true } }, + testFn, + ); +} + +Deno.test( + { permissions: { net: true } }, + async function httpServerConcurrentRequests() { + const ac = new AbortController(); + const { resolve } = Promise.withResolvers<void>(); + + let reqCount = -1; + let timerId: number | undefined; + const server = Deno.serve({ + handler: (_req) => { + reqCount++; + if (reqCount === 0) { + const msg = new TextEncoder().encode("data: hello\r\n\r\n"); + // SSE + const body = new ReadableStream({ + start(controller) { + timerId = setInterval(() => { + controller.enqueue(msg); + }, 1000); + }, + cancel() { + if (typeof timerId === "number") { + clearInterval(timerId); + } + }, + }); + return new Response(body, { + headers: { + "Content-Type": "text/event-stream", + }, + }); + } + + return new Response(`hello ${reqCount}`); + }, + port: servePort, + signal: ac.signal, + onListen: onListen(resolve), + onError: createOnErrorCb(ac), + }); + + const sseRequest = await fetch(`http://localhost:${servePort}/`); + + const decoder = new TextDecoder(); + const stream = sseRequest.body!.getReader(); + { + const { done, value } = await stream.read(); + assert(!done); + assertEquals(decoder.decode(value), "data: hello\r\n\r\n"); + } + + const helloRequest = await fetch(`http://localhost:${servePort}/`); + assertEquals(helloRequest.status, 200); + assertEquals(await helloRequest.text(), "hello 1"); + + { + const { done, value } = await stream.read(); + assert(!done); + assertEquals(decoder.decode(value), "data: hello\r\n\r\n"); + } + + await stream.cancel(); + clearInterval(timerId); + ac.abort(); + await server.finished; + }, +); + +Deno.test( + { permissions: { net: true } }, + async function serveWithPrototypePollution() { + const originalThen = Promise.prototype.then; + const originalSymbolIterator = Array.prototype[Symbol.iterator]; + try { + Promise.prototype.then = Array.prototype[Symbol.iterator] = () => { + throw new Error(); + }; + const ac = new AbortController(); + const { resolve } = Promise.withResolvers<void>(); + const server = Deno.serve({ + handler: (_req) => new Response("ok"), + hostname: "localhost", + port: servePort, + signal: ac.signal, + onListen: onListen(resolve), + onError: createOnErrorCb(ac), + }); + ac.abort(); + await server.finished; + } finally { + Promise.prototype.then = originalThen; + Array.prototype[Symbol.iterator] = originalSymbolIterator; + } + }, +); + +// https://github.com/denoland/deno/issues/15549 +Deno.test( + { permissions: { net: true } }, + async function testIssue15549() { + const ac = new AbortController(); + const { promise, resolve } = Promise.withResolvers<void>(); + let count = 0; + const server = Deno.serve({ + async onListen({ port }: { port: number }) { + const res1 = await fetch(`http://localhost:${port}/`); + assertEquals(await res1.text(), "hello world 1"); + + const res2 = await fetch(`http://localhost:${port}/`); + assertEquals(await res2.text(), "hello world 2"); + + resolve(); + ac.abort(); + }, + signal: ac.signal, + }, () => { + count++; + return new Response(`hello world ${count}`); + }); + + await promise; + await server.finished; + }, +); + +// https://github.com/denoland/deno/issues/15858 +Deno.test( + "Clone should work", + { permissions: { net: true } }, + async function httpServerCanCloneRequest() { + const ac = new AbortController(); + const { promise, resolve } = Promise.withResolvers<number>(); + + const server = Deno.serve({ + handler: async (req) => { + const cloned = req.clone(); + assertEquals(req.headers, cloned.headers); + + assertEquals(cloned.url, req.url); + assertEquals(cloned.cache, req.cache); + assertEquals(cloned.destination, req.destination); + assertEquals(cloned.headers, req.headers); + assertEquals(cloned.integrity, req.integrity); + assertEquals(cloned.isHistoryNavigation, req.isHistoryNavigation); + assertEquals(cloned.isReloadNavigation, req.isReloadNavigation); + assertEquals(cloned.keepalive, req.keepalive); + assertEquals(cloned.method, req.method); + assertEquals(cloned.mode, req.mode); + assertEquals(cloned.redirect, req.redirect); + assertEquals(cloned.referrer, req.referrer); + assertEquals(cloned.referrerPolicy, req.referrerPolicy); + + // both requests can read body + await req.text(); + await cloned.json(); + + return new Response("ok"); + }, + signal: ac.signal, + onListen: ({ port }: { port: number }) => resolve(port), + onError: createOnErrorCb(ac), + }); + + try { + const port = await promise; + const resp = await fetch(`http://localhost:${port}/`, { + headers: { connection: "close" }, + method: "POST", + body: '{"sus":true}', + }); + const text = await resp.text(); + assertEquals(text, "ok"); + } finally { + ac.abort(); + await server.finished; + } + }, +); + +// https://fetch.spec.whatwg.org/#dom-request-clone +Deno.test( + "Throw if disturbed", + { permissions: { net: true } }, + async function shouldThrowIfBodyIsUnusableDisturbed() { + const ac = new AbortController(); + const { promise, resolve } = Promise.withResolvers<number>(); + + const server = Deno.serve({ + handler: async (req) => { + await req.text(); + + try { + req.clone(); + fail(); + } catch (cloneError) { + assert(cloneError instanceof TypeError); + assert( + cloneError.message.endsWith("Body is unusable."), + ); + + ac.abort(); + await server.finished; + } + + return new Response("ok"); + }, + signal: ac.signal, + onListen: ({ port }: { port: number }) => resolve(port), + }); + + try { + const port = await promise; + await fetch(`http://localhost:${port}/`, { + headers: { connection: "close" }, + method: "POST", + body: '{"bar":true}', + }); + fail(); + } catch (clientError) { + assert(clientError instanceof TypeError); + assert( + clientError.message.endsWith( + "connection closed before message completed", + ), + ); + } finally { + ac.abort(); + await server.finished; + } + }, +); + +// https://fetch.spec.whatwg.org/#dom-request-clone +Deno.test({ + name: "Throw if locked", + permissions: { net: true }, + fn: async function shouldThrowIfBodyIsUnusableLocked() { + const ac = new AbortController(); + const { promise, resolve } = Promise.withResolvers<number>(); + + const server = Deno.serve({ + handler: async (req) => { + const _reader = req.body?.getReader(); + + try { + req.clone(); + fail(); + } catch (cloneError) { + assert(cloneError instanceof TypeError); + assert( + cloneError.message.endsWith("Body is unusable."), + ); + + ac.abort(); + await server.finished; + } + return new Response("ok"); + }, + signal: ac.signal, + onListen: ({ port }: { port: number }) => resolve(port), + }); + + try { + const port = await promise; + await fetch(`http://localhost:${port}/`, { + headers: { connection: "close" }, + method: "POST", + body: '{"bar":true}', + }); + fail(); + } catch (clientError) { + assert(clientError instanceof TypeError); + assert( + clientError.message.endsWith( + "connection closed before message completed", + ), + ); + } finally { + ac.abort(); + await server.finished; + } + }, +}); + +// Checks large streaming response +// https://github.com/denoland/deno/issues/16567 +Deno.test( + { permissions: { net: true } }, + async function testIssue16567() { + const ac = new AbortController(); + const { promise, resolve } = Promise.withResolvers<void>(); + const server = Deno.serve({ + async onListen({ port }) { + const res1 = await fetch(`http://localhost:${port}/`); + assertEquals((await res1.text()).length, 40 * 50_000); + + resolve(); + ac.abort(); + }, + signal: ac.signal, + }, () => + new Response( + new ReadableStream({ + start(c) { + // 2MB "a...a" response with 40 chunks + for (const _ of Array(40)) { + c.enqueue(new Uint8Array(50_000).fill(97)); + } + c.close(); + }, + }), + )); + + await promise; + await server.finished; + }, +); + +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<number | null> { + 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()); +} + +// TODO(mmastrac): curl on Windows CI stopped supporting --http2? +Deno.test( + { + permissions: { net: true, run: true }, + ignore: Deno.build.os === "windows", + }, + async function httpServeCurlH2C() { + const ac = new AbortController(); + const server = Deno.serve( + { port: servePort, signal: ac.signal }, + () => new Response("hello world!"), + ); + + assertEquals( + "hello world!", + await curlRequest([`http://localhost:${servePort}/path`]), + ); + assertEquals( + "hello world!", + await curlRequest([`http://localhost:${servePort}/path`, "--http2"]), + ); + assertEquals( + "hello world!", + await curlRequest([ + `http://localhost:${servePort}/path`, + "--http2", + "--http2-prior-knowledge", + ]), + ); + + ac.abort(); + await server.finished; + }, +); + +// TODO(mmastrac): This test should eventually use fetch, when we support trailers there. +// This test is ignored because it's flaky and relies on cURL's verbose output. +Deno.test( + { permissions: { net: true, run: true, read: true }, ignore: true }, + async function httpServerTrailers() { + const ac = new AbortController(); + const { resolve } = Promise.withResolvers<void>(); + + const server = Deno.serve({ + handler: () => { + const response = new Response("Hello World", { + headers: { + "trailer": "baz", + "transfer-encoding": "chunked", + "foo": "bar", + }, + }); + addTrailers(response, [["baz", "why"]]); + return response; + }, + port: servePort, + signal: ac.signal, + onListen: onListen(resolve), + onError: createOnErrorCb(ac), + }); + + // We don't have a great way to access this right now, so just fetch the trailers with cURL + const [_, stderr] = await curlRequestWithStdErr([ + `http://localhost:${servePort}/path`, + "-v", + "--http2", + "--http2-prior-knowledge", + ]); + assertMatch(stderr, /baz: why/); + ac.abort(); + await server.finished; + }, +); + +// TODO(mmastrac): curl on CI stopped supporting --http2? +Deno.test( + { + permissions: { + net: true, + run: true, + read: true, + }, + ignore: Deno.build.os === "windows", + }, + async function httpsServeCurlH2C() { + const ac = new AbortController(); + const server = Deno.serve( + { + signal: ac.signal, + port: servePort, + cert: Deno.readTextFileSync("tests/testdata/tls/localhost.crt"), + key: Deno.readTextFileSync("tests/testdata/tls/localhost.key"), + }, + () => new Response("hello world!"), + ); + + assertEquals( + "hello world!", + await curlRequest([`https://localhost:${servePort}/path`, "-k"]), + ); + assertEquals( + "hello world!", + await curlRequest([ + `https://localhost:${servePort}/path`, + "-k", + "--http2", + ]), + ); + assertEquals( + "hello world!", + await curlRequest([ + `https://localhost:${servePort}/path`, + "-k", + "--http2", + "--http2-prior-knowledge", + ]), + ); + + ac.abort(); + await server.finished; + }, +); + +async function curlRequest(args: string[]) { + const { success, stdout, stderr } = await new Deno.Command("curl", { + args, + stdout: "piped", + stderr: "piped", + }).output(); + assert( + success, + `Failed to cURL ${args}: stdout\n\n${stdout}\n\nstderr:\n\n${stderr}`, + ); + return new TextDecoder().decode(stdout); +} + +async function curlRequestWithStdErr(args: string[]) { + const { success, stdout, stderr } = await new Deno.Command("curl", { + args, + stdout: "piped", + stderr: "piped", + }).output(); + assert( + success, + `Failed to cURL ${args}: stdout\n\n${stdout}\n\nstderr:\n\n${stderr}`, + ); + return [new TextDecoder().decode(stdout), new TextDecoder().decode(stderr)]; +} + +Deno.test("Deno.HttpServer is not thenable", async () => { + // deno-lint-ignore require-await + async function serveTest() { + const server = Deno.serve({ port: servePort }, (_) => new Response("")); + assert(!("then" in server)); + return server; + } + const server = await serveTest(); + await server.shutdown(); +}); + +Deno.test( + { + ignore: Deno.build.os === "windows", + permissions: { run: true, read: true, write: true }, + }, + async function httpServerUnixDomainSocket() { + const { promise, resolve } = Promise.withResolvers<{ path: string }>(); + const ac = new AbortController(); + const filePath = tmpUnixSocketPath(); + const server = Deno.serve( + { + signal: ac.signal, + path: filePath, + onListen(info) { + resolve(info); + }, + onError: createOnErrorCb(ac), + }, + (_req, { remoteAddr }) => { + assertEquals(remoteAddr, { path: filePath, transport: "unix" }); + return new Response("hello world!"); + }, + ); + + assertEquals(await promise, { path: filePath }); + assertEquals( + "hello world!", + await curlRequest(["--unix-socket", filePath, "http://localhost"]), + ); + ac.abort(); + await server.finished; + }, +); + +// serve Handler must return Response class or promise that resolves Response class +Deno.test( + { permissions: { net: true, run: true } }, + async function handleServeCallbackReturn() { + const deferred = Promise.withResolvers<void>(); + const listeningDeferred = Promise.withResolvers<void>(); + const ac = new AbortController(); + + const server = Deno.serve( + { + port: servePort, + onListen: onListen(listeningDeferred.resolve), + signal: ac.signal, + onError: (error) => { + assert(error instanceof TypeError); + assert( + error.message === + "Return value from serve handler must be a response or a promise resolving to a response", + ); + deferred.resolve(); + return new Response("Customized Internal Error from onError"); + }, + }, + () => { + // Trick the typechecker + return <Response> <unknown> undefined; + }, + ); + await listeningDeferred.promise; + const respText = await curlRequest([`http://localhost:${servePort}`]); + await deferred.promise; + ac.abort(); + await server.finished; + assert(respText === "Customized Internal Error from onError"); + }, +); + +// onError Handler must return Response class or promise that resolves Response class +Deno.test( + { permissions: { net: true, run: true } }, + async function handleServeErrorCallbackReturn() { + const { promise, resolve } = Promise.withResolvers<void>(); + const ac = new AbortController(); + + const server = Deno.serve( + { + port: servePort, + onListen: onListen(resolve), + signal: ac.signal, + onError: () => { + // Trick the typechecker + return <Response> <unknown> undefined; + }, + }, + () => { + // Trick the typechecker + return <Response> <unknown> undefined; + }, + ); + await promise; + const respText = await curlRequest([`http://localhost:${servePort}`]); + ac.abort(); + await server.finished; + assert(respText === "Internal Server Error"); + }, +); |