summaryrefslogtreecommitdiff
path: root/cli/tests/unit/serve_test.ts
diff options
context:
space:
mode:
Diffstat (limited to 'cli/tests/unit/serve_test.ts')
-rw-r--r--cli/tests/unit/serve_test.ts3932
1 files changed, 0 insertions, 3932 deletions
diff --git a/cli/tests/unit/serve_test.ts b/cli/tests/unit/serve_test.ts
deleted file mode 100644
index b5c966d6f..000000000
--- a/cli/tests/unit/serve_test.ts
+++ /dev/null
@@ -1,3932 +0,0 @@
-// 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("cli/tests/testdata/tls/localhost.crt"),
- key: Deno.readTextFileSync("cli/tests/testdata/tls/localhost.key"),
- });
-
- await promise;
- const caCert = Deno.readTextFileSync("cli/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("cli/tests/testdata/tls/localhost.crt"),
- key: Deno.readTextFileSync("cli/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");
- },
-);