1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-11-23 15:16:54 -05:00
denoland-deno/cli/tests/unit/serve_test.ts
Luca Casonato 430b63c2c4
perf: improve async op santizer speed and accuracy (#20501)
This commit improves async op sanitizer speed by only delaying metrics
collection if there are pending ops. This
results in a speedup of around 30% for small CPU bound unit tests.

It performs this check and possible delay on every collection now,
fixing an issue with parent test leaks into steps.
2023-09-16 07:48:31 +02:00

3696 lines
103 KiB
TypeScript

// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
import {
assertMatch,
assertRejects,
} from "../../../test_util/std/testing/asserts.ts";
import { Buffer, BufReader, BufWriter } from "../../../test_util/std/io/mod.ts";
import { TextProtoReader } from "../testdata/run/textproto.ts";
import {
assert,
assertEquals,
assertStringIncludes,
assertThrows,
Deferred,
deferred,
execCode,
fail,
} 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<T>(
p: Deferred<T>,
): ({ hostname, port }: { hostname: string; port: number }) => void {
return () => {
p.resolve();
};
}
async function makeServer(
handler: (req: Request) => Response | Promise<Response>,
): Promise<
{ finished: Promise<void>; abort: () => void; shutdown: () => Promise<void> }
> {
const ac = new AbortController();
const listeningPromise = deferred();
const server = Deno.serve({
handler,
port: servePort,
signal: ac.signal,
onListen: onListen(listeningPromise),
});
await listeningPromise;
return {
finished: server.finished,
abort() {
ac.abort();
},
async shutdown() {
await server.shutdown();
},
};
}
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 promiseQueue: { input: Deferred<string>; out: Deferred<void> }[] = [];
const { finished, abort } = await makeServer((_req) => {
const { input, out } = promiseQueue.shift()!;
return new Response(
new ReadableStream({
async start(controller) {
controller.enqueue(new Uint8Array([46]));
out.resolve(undefined);
controller.enqueue(encoder.encode(await input));
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: deferred<string>(), out: deferred<void>() };
promiseQueue.push(deferred1);
const deferred2 = { input: deferred<string>(), out: deferred<void>() };
promiseQueue.push(deferred2);
const deferred3 = { input: deferred<string>(), out: deferred<void>() };
promiseQueue.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;
// 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 promiseQueue: { input: Deferred<string>; out: Deferred<void> }[] = [];
const { finished, shutdown } = await makeServer((_req) => {
const { input, out } = promiseQueue.shift()!;
return new Response(
new ReadableStream({
async start(controller) {
controller.enqueue(new Uint8Array([46]));
out.resolve(undefined);
controller.enqueue(encoder.encode(await input));
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: deferred<string>(), out: deferred<void>() };
promiseQueue.push(deferred1);
const deferred2 = { input: deferred<string>(), out: deferred<void>() };
promiseQueue.push(deferred2);
const deferred3 = { input: deferred<string>(), out: deferred<void>() };
promiseQueue.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;
// 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 waitForRequest = deferred();
const { finished, shutdown } = await makeServer(async (_req) => {
waitForRequest.resolve(null);
await new Promise((r) => setTimeout(r, 10));
return new Response((await makeTempFile(1024 * 1024)).readable);
});
const f = fetch(`http://localhost:${servePort}`);
await waitForRequest;
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 = deferred();
const waitForRequest = deferred();
const { finished, shutdown } = await makeServer(async (_req) => {
waitForRequest.resolve(null);
await waitForAbort;
await new Promise((r) => setTimeout(r, 10));
return new Response((await makeTempFile(1024 * 1024)).readable);
});
const f = fetch(`http://localhost:${servePort}`);
await waitForRequest;
const s = shutdown();
waitForAbort.resolve(null);
assertEquals((await (await f).text()).length, 1048576);
await s;
await finished;
},
);
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 listeningPromise = deferred();
const server = Deno.serve({
handler: (_req) => new Response("ok"),
hostname: "localhost",
port: servePort,
signal: ac.signal,
onListen: onListen(listeningPromise),
onError: createOnErrorCb(ac),
});
await listeningPromise;
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 listeningPromise = deferred();
const server = Deno.serve({
handler: (_req) => new Response("ok"),
hostname: "localhost",
port: servePort,
signal: ac.signal,
onListen: onListen(listeningPromise),
onError: createOnErrorCb(ac),
});
await listeningPromise;
assertThrows(
() =>
Deno.serve({
handler: (_req) => new Response("ok"),
hostname: "localhost",
port: servePort,
signal: ac.signal,
onListen: onListen(listeningPromise),
onError: createOnErrorCb(ac),
}),
Deno.errors.AddrInUse,
);
ac.abort();
await server.finished;
});
Deno.test({ permissions: { net: true } }, async function httpServerBasic() {
const ac = new AbortController();
const promise = deferred();
const listeningPromise = deferred();
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");
promise.resolve();
return new Response("Hello World", { headers: { "foo": "bar" } });
},
port: servePort,
signal: ac.signal,
onListen: onListen(listeningPromise),
onError: createOnErrorCb(ac),
});
await listeningPromise;
const resp = await fetch(`http://127.0.0.1:${servePort}/`, {
headers: { "connection": "close" },
});
await 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 promise = deferred();
const listeningPromise = deferred();
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");
promise.resolve();
return new Response("Hello World", { headers: { "foo": "bar" } });
},
createOnErrorCb(ac),
onListen(listeningPromise),
);
await listeningPromise;
const resp = await fetch(`http://127.0.0.1:${servePort}/`, {
headers: { "connection": "close" },
});
await 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 promise = deferred();
const listeningPromise = deferred();
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");
promise.resolve();
return new Response("Hello World", { headers: { "foo": "bar" } });
},
createOnErrorCb(ac),
onListen(listeningPromise),
);
const resp = await fetchPromise;
await 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 listeningPromise = deferred();
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(listeningPromise),
onError: () => {
return new Response("failed: " + requestStash!.url, { status: 500 });
},
});
await listeningPromise;
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 listeningPromise = deferred();
// 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(listeningPromise),
onError: () => {
throw "again";
},
});
await listeningPromise;
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 promise = deferred();
const listeningPromise = deferred();
const server = Deno.serve({
port: servePort,
signal: ac.signal,
onListen: onListen(listeningPromise),
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(), "");
promise.resolve();
return new Response("Hello World", { headers: { "foo": "bar" } });
});
await listeningPromise;
const resp = await fetch(`http://127.0.0.1:${servePort}/`, {
headers: { "connection": "close" },
});
await 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 promise = deferred();
const listeningPromise = deferred();
const server = Deno.serve({
port: servePort,
signal: ac.signal,
onListen: onListen(listeningPromise),
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(), "");
promise.resolve();
return new Response("Hello World", { headers: { "foo": "bar" } });
});
await listeningPromise;
const resp = await fetch(`http://127.0.0.1:${servePort}/`, {
headers: { "connection": "close" },
});
await 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 promise = deferred();
const listeningPromise = deferred();
const ac = new AbortController();
let headers: Headers;
const server = Deno.serve({
handler: async (request) => {
await request.text();
headers = request.headers;
promise.resolve();
return new Response("");
},
port: servePort,
signal: ac.signal,
onListen: onListen(listeningPromise),
onError: createOnErrorCb(ac),
});
await listeningPromise;
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 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 listeningPromise: Deferred<number> = deferred();
const urlPromise = deferred();
const ac = new AbortController();
const server = Deno.serve({
handler: (request: Request) => {
urlPromise.resolve(request.url);
return new Response("");
},
port: 0,
signal: ac.signal,
onListen: ({ port }: { port: number }) => {
listeningPromise.resolve(port);
},
onError: createOnErrorCb(ac),
});
const port = await listeningPromise;
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 urlPromise, 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 promise = deferred();
const ac = new AbortController();
const listeningPromise = deferred();
const server = Deno.serve({
handler: (request) => {
assertEquals(request.body, null);
promise.resolve();
return new Response("", { headers: {} });
},
port: servePort,
signal: ac.signal,
onListen: onListen(listeningPromise),
onError: createOnErrorCb(ac),
});
await listeningPromise;
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 promise;
ac.abort();
await server.finished;
},
);
Deno.test(
{ permissions: { net: true } },
async function httpServerAbortedRequestBody() {
const promise = deferred();
const ac = new AbortController();
const listeningPromise = deferred();
const server = Deno.serve({
handler: async (request) => {
await assertRejects(async () => {
await request.text();
});
promise.resolve();
// Not actually used
return new Response();
},
port: servePort,
signal: ac.signal,
onListen: onListen(listeningPromise),
onError: createOnErrorCb(ac),
});
await listeningPromise;
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 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 listeningPromise = deferred();
const server = Deno.serve({
handler: (_request) => {
return new Response(makeStream(count, delay));
},
port: servePort,
signal: ac.signal,
onListen: onListen(listeningPromise),
onError: createOnErrorCb(ac),
});
try {
await listeningPromise;
const client = Deno.createHttpClient({});
const resp = await fetch(`http://127.0.0.1:${servePort}/`, { client });
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);
}
client.close();
} 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 listeningPromise = deferred();
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(listeningPromise),
onError: createOnErrorCb(ac),
});
await listeningPromise;
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 listeningPromise = deferred();
const server = Deno.serve({
handler: () => new Response("ok"),
port: servePort,
signal: ac.signal,
onListen: onListen(listeningPromise),
onError: createOnErrorCb(ac),
});
await listeningPromise;
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 listeningPromise = deferred();
const requestPromise = deferred();
const responsePromise = deferred();
const server = Deno.serve({
handler: async () => {
requestPromise.resolve();
await new Promise((r) => setTimeout(r, 500));
responsePromise.resolve();
return new Response("ok");
},
port: servePort,
signal: ac.signal,
onListen: onListen(listeningPromise),
onError: createOnErrorCb(ac),
});
await listeningPromise;
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 requestPromise;
conn.close();
await responsePromise;
ac.abort();
await server.finished;
});
// FIXME:
Deno.test(
{ permissions: { net: true } },
async function httpServerEmptyBlobResponse() {
const ac = new AbortController();
const listeningPromise = deferred();
const server = Deno.serve({
handler: () => new Response(new Blob([])),
port: servePort,
signal: ac.signal,
onListen: onListen(listeningPromise),
onError: createOnErrorCb(ac),
});
await listeningPromise;
const client = Deno.createHttpClient({});
const resp = await fetch(`http://127.0.0.1:${servePort}/`, { client });
const respBody = await resp.text();
assertEquals("", respBody);
ac.abort();
await server.finished;
client.close();
},
);
// https://github.com/denoland/deno/issues/17291
Deno.test(
{ permissions: { net: true } },
async function httpServerIncorrectChunkedResponse() {
const ac = new AbortController();
const listeningPromise = deferred();
const errorPromise = deferred();
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(listeningPromise),
onError: (err) => {
const errResp = new Response(
`Internal server error: ${(err as Error).message}`,
{ status: 500 },
);
errorPromise.resolve();
return errResp;
},
});
await listeningPromise;
const client = Deno.createHttpClient({});
const resp = await fetch(`http://127.0.0.1:${servePort}/`, { client });
// Incorrectly implemented reader ReadableStream should reject.
assertStringIncludes(await resp.text(), "Failed to execute 'enqueue'");
await errorPromise;
ac.abort();
await server.finished;
client.close();
},
);
Deno.test(
{ permissions: { net: true } },
async function httpServerCorrectLengthForUnicodeString() {
const ac = new AbortController();
const listeningPromise = deferred();
const server = Deno.serve({
handler: () => new Response("韓國".repeat(10)),
port: servePort,
signal: ac.signal,
onListen: onListen(listeningPromise),
onError: createOnErrorCb(ac),
});
await listeningPromise;
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 listeningPromise = deferred();
const done = deferred();
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 = () => done.resolve();
return response;
},
port: servePort,
signal: ac.signal,
onListen: onListen(listeningPromise),
onError: createOnErrorCb(ac),
});
await listeningPromise;
const def = deferred();
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;
await done;
ac.abort();
await server.finished;
});
Deno.test(
{ permissions: { net: true } },
async function httpServerWebSocketRaw() {
const ac = new AbortController();
const listeningPromise = deferred();
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(listeningPromise),
onError: createOnErrorCb(ac),
});
await listeningPromise;
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 = deferred();
const listeningPromise = deferred();
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(listeningPromise),
onError: createOnErrorCb(ac),
});
await listeningPromise;
const def = deferred();
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;
await done;
ac.abort();
await server.finished;
},
);
Deno.test(
{ permissions: { net: true } },
async function httpServerWebSocketCloseFast() {
const ac = new AbortController();
const done = deferred();
const listeningPromise = deferred();
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(listeningPromise),
onError: createOnErrorCb(ac),
});
await listeningPromise;
const def = deferred();
const ws = new WebSocket(`ws://localhost:${servePort}`);
ws.onerror = (e) => {
console.error(e);
fail();
};
ws.onclose = () => def.resolve();
await def;
await done;
ac.abort();
await server.finished;
},
);
Deno.test(
{ permissions: { net: true } },
async function httpServerWebSocketCanAccessRequest() {
const ac = new AbortController();
const done = deferred();
const listeningPromise = deferred();
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(listeningPromise),
onError: createOnErrorCb(ac),
});
await listeningPromise;
const def = deferred();
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;
await done;
ac.abort();
await server.finished;
},
);
Deno.test(
{ permissions: { net: true } },
async function httpVeryLargeRequest() {
const promise = deferred();
const listeningPromise = deferred();
const ac = new AbortController();
let headers: Headers;
const server = Deno.serve({
handler: (request) => {
headers = request.headers;
promise.resolve();
return new Response("");
},
port: servePort,
signal: ac.signal,
onListen: onListen(listeningPromise),
onError: createOnErrorCb(ac),
});
await listeningPromise;
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 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 promise = deferred();
const listeningPromise = deferred();
const ac = new AbortController();
let headers: Headers;
let text: string;
const server = Deno.serve({
handler: async (request) => {
headers = request.headers;
text = await request.text();
promise.resolve();
return new Response("");
},
port: servePort,
signal: ac.signal,
onListen: onListen(listeningPromise),
onError: createOnErrorCb(ac),
});
await listeningPromise;
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 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 promise = deferred();
const ac = new AbortController();
const listeningPromise = deferred();
const server = Deno.serve({
handler: () => {
promise.resolve();
return new Response("");
},
port: servePort,
signal: ac.signal,
onListen: onListen(listeningPromise),
onError: createOnErrorCb(ac),
});
await listeningPromise;
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 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 = deferred();
const ac = new AbortController();
const server = Deno.serve(
{ port: servePort, signal: ac.signal },
(request: Request) => {
assert(request.body);
promise.resolve();
return new Response(request.body);
},
);
const { readable, writable } = new TransformStream();
const client = Deno.createHttpClient({});
const resp = await fetch(`http://127.0.0.1:${servePort}/`, {
method: "POST",
body: readable,
client,
});
await promise;
assert(resp.body);
await testDuplex(resp.body.getReader(), writable.getWriter());
ac.abort();
await server.finished;
client.close();
},
);
// 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 = deferred();
const ac = new AbortController();
const server = Deno.serve(
{ port: servePort, signal: ac.signal },
(request: Request) => {
assert(request.body);
promise.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 client = Deno.createHttpClient({});
const resp = await fetch(`http://127.0.0.1:${servePort}/`, {
method: "POST",
body: readable,
client,
});
await promise;
assert(resp.body);
await testDuplex(resp.body.getReader(), writable.getWriter());
ac.abort();
await server.finished;
client.close();
},
);
Deno.test(
{ permissions: { net: true } },
// Issue: https://github.com/denoland/deno/issues/10930
async function httpServerStreamingResponse() {
// This test enqueues a single chunk for readable
// stream and waits for client to read that chunk and signal
// it before enqueueing subsequent chunk. Issue linked above
// presented a situation where enqueued chunks were not
// written to the HTTP connection until the next chunk was enqueued.
const listeningPromise = deferred();
const promise = deferred();
const ac = new AbortController();
let counter = 0;
const deferreds = [
deferred(),
deferred(),
deferred(),
];
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];
controller.enqueue(`${counter}\n`);
counter++;
},
}).pipeThrough(new TextEncoderStream());
}
const server = Deno.serve({
handler: () => {
promise.resolve();
return new Response(periodicStream());
},
port: servePort,
signal: ac.signal,
onListen: onListen(listeningPromise),
onError: createOnErrorCb(ac),
});
await listeningPromise;
// start a client
const clientConn = await Deno.connect({ port: servePort });
const r1 = await writeRequest(clientConn);
assertEquals(r1, "0\n1\n2\n");
ac.abort();
await 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 listeningPromise = deferred();
const promise = deferred();
const ac = new AbortController();
const server = Deno.serve({
handler: (request) => {
assertEquals(request.headers.get("X-Header-Test"), "á");
promise.resolve();
return new Response("hello", { headers: { "X-Header-Test": "Æ" } });
},
port: servePort,
signal: ac.signal,
onListen: onListen(listeningPromise),
onError: createOnErrorCb(ac),
});
await listeningPromise;
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 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 promise = deferred();
const listeningPromise = deferred();
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(), "");
promise.resolve();
return new Response("11");
},
port: servePort,
signal: ac.signal,
onListen: onListen(listeningPromise),
onError: createOnErrorCb(ac),
});
await listeningPromise;
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 promise;
ac.abort();
await server.finished;
},
);
Deno.test(
{ permissions: { net: true } },
async function httpCookieConcatenation() {
const promise = deferred();
const listeningPromise = deferred();
const ac = new AbortController();
const server = Deno.serve({
handler: async (request) => {
assertEquals(await request.text(), "");
assertEquals(request.headers.get("cookie"), "foo=bar; bar=foo");
promise.resolve();
return new Response("ok");
},
port: servePort,
signal: ac.signal,
onListen: onListen(listeningPromise),
onError: createOnErrorCb(ac),
reusePort: true,
});
await listeningPromise;
const resp = await fetch(`http://127.0.0.1:${servePort}/`, {
headers: [
["connection", "close"],
["cookie", "foo=bar"],
["cookie", "bar=foo"],
],
});
await 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 promise = deferred();
const listeningPromise = deferred();
const ac = new AbortController();
const hostname = "localhost";
const server = Deno.serve({
handler: () => {
promise.resolve();
return new Response("ok");
},
port: servePort,
signal: ac.signal,
onListen: onListen(listeningPromise),
onError: createOnErrorCb(ac),
});
await listeningPromise;
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 promise;
ac.abort();
await server.finished;
},
);
// FIXME:
Deno.test(
{ permissions: { net: true } },
async function httpServerRespondNonAsciiUint8Array() {
const promise = deferred();
const listeningPromise = deferred();
const ac = new AbortController();
const server = Deno.serve({
handler: (request) => {
assertEquals(request.body, null);
promise.resolve();
return new Response(new Uint8Array([128]));
},
port: servePort,
signal: ac.signal,
onListen: onListen(listeningPromise),
onError: createOnErrorCb(ac),
});
await listeningPromise;
const resp = await fetch(`http://localhost:${servePort}/`);
await 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 promise = deferred();
const listeningPromise = deferred();
const ac = new AbortController();
const server = Deno.serve({
handler: (request) => {
assertEquals(request.method, "GET");
assertEquals(request.headers.get("host"), "deno.land");
promise.resolve();
return new Response("ok");
},
port: servePort,
signal: ac.signal,
onListen: onListen(listeningPromise),
onError: createOnErrorCb(ac),
});
await listeningPromise;
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 promise;
conn.close();
ac.abort();
await server.finished;
},
);
Deno.test(
{ permissions: { net: true } },
async function httpServerParseHeaderHtabs() {
const promise = deferred();
const listeningPromise = deferred();
const ac = new AbortController();
const server = Deno.serve({
handler: (request) => {
assertEquals(request.method, "GET");
assertEquals(request.headers.get("server"), "hello\tworld");
promise.resolve();
return new Response("ok");
},
port: servePort,
signal: ac.signal,
onListen: onListen(listeningPromise),
onError: createOnErrorCb(ac),
});
await listeningPromise;
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 promise;
conn.close();
ac.abort();
await server.finished;
},
);
Deno.test(
{ permissions: { net: true } },
async function httpServerGetShouldIgnoreBody() {
const promise = deferred();
const listeningPromise = deferred();
const ac = new AbortController();
const server = Deno.serve({
handler: async (request) => {
assertEquals(request.method, "GET");
assertEquals(await request.text(), "");
promise.resolve();
return new Response("ok");
},
port: servePort,
signal: ac.signal,
onListen: onListen(listeningPromise),
onError: createOnErrorCb(ac),
});
await listeningPromise;
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 promise;
conn.close();
ac.abort();
await server.finished;
},
);
Deno.test(
{ permissions: { net: true } },
async function httpServerPostWithBody() {
const promise = deferred();
const listeningPromise = deferred();
const ac = new AbortController();
const server = Deno.serve({
handler: async (request) => {
assertEquals(request.method, "POST");
assertEquals(await request.text(), "I'm a good request.");
promise.resolve();
return new Response("ok");
},
port: servePort,
signal: ac.signal,
onListen: onListen(listeningPromise),
onError: createOnErrorCb(ac),
});
await listeningPromise;
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 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 promise = deferred();
const ac = new AbortController();
const listeningPromise = deferred();
const server = Deno.serve({
handler: (request) => {
assertEquals(request.method, "GET");
promise.resolve();
return new Response(testCase.body, testCase.headers ?? {});
},
port: servePort,
signal: ac.signal,
onListen: onListen(listeningPromise),
onError: createOnErrorCb(ac),
});
await listeningPromise;
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 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 promise = deferred();
const listeningPromise = deferred();
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");
promise.resolve();
return new Response("ok");
},
port: servePort,
signal: ac.signal,
onListen: onListen(listeningPromise),
onError: createOnErrorCb(ac),
});
await listeningPromise;
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 promise;
conn.close();
ac.abort();
await server.finished;
},
);
Deno.test(
{ permissions: { net: true } },
async function httpServerPostWithInvalidPrefixContentLength() {
const ac = new AbortController();
const listeningPromise = deferred();
const server = Deno.serve({
handler: () => {
throw new Error("unreachable");
},
port: servePort,
signal: ac.signal,
onListen: onListen(listeningPromise),
onError: createOnErrorCb(ac),
});
await listeningPromise;
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 promise = deferred();
const ac = new AbortController();
const listeningPromise = deferred();
const server = Deno.serve({
handler: async (request) => {
assertEquals(request.method, "POST");
assertEquals(await request.text(), "qwert");
promise.resolve();
return new Response("ok");
},
port: servePort,
signal: ac.signal,
onListen: onListen(listeningPromise),
onError: createOnErrorCb(ac),
});
await listeningPromise;
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 promise;
conn.close();
ac.abort();
await server.finished;
},
);
Deno.test(
{ permissions: { net: true } },
async function httpServerPostWithIncompleteBody() {
const promise = deferred();
const ac = new AbortController();
const listeningPromise = deferred();
const server = Deno.serve({
handler: async (r) => {
promise.resolve();
assertEquals(await r.text(), "12345");
return new Response("ok");
},
port: servePort,
signal: ac.signal,
onListen: onListen(listeningPromise),
onError: createOnErrorCb(ac),
});
await listeningPromise;
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 promise;
conn.close();
ac.abort();
await server.finished;
},
);
Deno.test(
{ permissions: { net: true } },
async function httpServerHeadResponseDoesntSendBody() {
const promise = deferred();
const ac = new AbortController();
const listeningPromise = deferred();
const server = Deno.serve({
handler: () => {
promise.resolve();
return new Response("NaN".repeat(100));
},
port: servePort,
signal: ac.signal,
onListen: onListen(listeningPromise),
onError: createOnErrorCb(ac),
});
await listeningPromise;
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 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();
const file = await Deno.open(tmpFile, { write: true, read: true });
const data = makeTempData(size);
await file.write(data);
file.close();
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,
},
];
for (const testCase of compressionTestCases) {
const name = `httpServerCompression${testCase.name}`;
Deno.test(
{ permissions: { net: true, write: true, read: true } },
{
[name]: async function () {
const promise = deferred();
const ac = new AbortController();
const listeningPromise = deferred();
const server = Deno.serve({
handler: async (_request) => {
const f = await makeTempFile(testCase.length);
promise.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(listeningPromise),
onError: createOnErrorCb(ac),
});
try {
await listeningPromise;
const client = Deno.createHttpClient({});
const resp = await fetch(`http://127.0.0.1:${servePort}/`, {
headers: testCase.in as HeadersInit,
client,
});
client.close();
await 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 promise = deferred();
const ac = new AbortController();
const listeningPromise = deferred();
const server = Deno.serve({
handler: async (request) => {
assertEquals(
new Uint8Array(await request.arrayBuffer()),
makeTempData(70 * 1024),
);
promise.resolve();
return new Response("ok");
},
port: servePort,
signal: ac.signal,
onListen: onListen(listeningPromise),
onError: createOnErrorCb(ac),
});
await listeningPromise;
const f = await makeTempFile(70 * 1024);
const response = await fetch(`http://localhost:${servePort}/`, {
method: "POST",
body: f.readable,
});
await promise;
assertEquals(response.status, 200);
assertEquals(await response.text(), "ok");
ac.abort();
await server.finished;
},
);
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
Deno.test({
permissions: { read: true, write: true, net: true },
name: `httpServerTcpCancellation_${url}`,
fn: async function () {
const ac = new AbortController();
const listeningPromise = deferred();
const waitForAbort = deferred();
const waitForRequest = deferred();
const server = Deno.serve({
port: servePort,
signal: ac.signal,
onListen: onListen(listeningPromise),
handler: async (req: Request) => {
waitForRequest.resolve();
await waitForAbort;
// Allocate the request body
let _body = req.body;
if (req.url.includes("/text")) {
return new Response("text");
} else if (req.url.includes("/file")) {
return new Response((await makeTempFile(1024)).readable);
} else if (req.url.includes("/stream")) {
return new Response(
new ReadableStream({
start(controller) {
_body = null;
controller.enqueue(new Uint8Array([1]));
controller.close();
},
}),
);
} else {
fail();
}
},
});
await listeningPromise;
// 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();
writer.write(new TextEncoder().encode(`POST /${url} HTTP/1.0\n\n`));
await waitForRequest;
writer.close();
// Give it a few milliseconds for the serve machinery to work
await new Promise((r) => setTimeout(r, 10));
waitForAbort.resolve();
// Give it a few milliseconds for the serve machinery to work
await new Promise((r) => setTimeout(r, 10));
// 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: { read: true, net: true } },
async function httpServerWithTls() {
const ac = new AbortController();
const listeningPromise = deferred();
const hostname = "127.0.0.1";
const server = Deno.serve({
handler: () => new Response("Hello World"),
hostname,
port: servePort,
signal: ac.signal,
onListen: onListen(listeningPromise),
onError: createOnErrorCb(ac),
cert: Deno.readTextFileSync("cli/tests/testdata/tls/localhost.crt"),
key: Deno.readTextFileSync("cli/tests/testdata/tls/localhost.key"),
});
await listeningPromise;
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 listeningPromise = deferred();
const promise = deferred();
const server = Deno.serve({
handler: async (req) => {
assertEquals(await req.text(), "");
promise.resolve();
return new Response("ok");
},
port: servePort,
signal: ac.signal,
onListen: onListen(listeningPromise),
onError: createOnErrorCb(ac),
});
await listeningPromise;
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 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 listeningPromise = deferred();
const server = Deno.serve({
handler: () => {
throw new Error("oops");
},
port: servePort,
signal: ac.signal,
onListen: onListen(listeningPromise),
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 listeningPromise;
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 listeningPromise = deferred();
const ac = new AbortController();
const server = Deno.serve({
handler: (_request) => new Response(null, { status: 204 }),
port: servePort,
signal: ac.signal,
onListen: onListen(listeningPromise),
onError: createOnErrorCb(ac),
});
try {
await listeningPromise;
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 promise = deferred();
const ac = new AbortController();
const listeningPromise = deferred();
const server = Deno.serve({
handler: () => {
promise.resolve();
return new Response(null, { status: 304 });
},
port: servePort,
signal: ac.signal,
onListen: onListen(listeningPromise),
onError: createOnErrorCb(ac),
});
await listeningPromise;
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 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 promise = deferred();
const ac = new AbortController();
const listeningPromise = deferred();
const server = Deno.serve({
handler: async (req) => {
promise.resolve();
assertEquals(await req.text(), "hello");
return new Response(null, { status: 304 });
},
port: servePort,
signal: ac.signal,
onListen: onListen(listeningPromise),
onError: createOnErrorCb(ac),
});
await listeningPromise;
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 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 promise = deferred();
const listeningPromise = deferred();
const ac = new AbortController();
const server = Deno.serve({
handler: async (req) => {
promise.resolve();
assertEquals(await req.text(), "");
return new Response(null, { status: 304 });
},
port: servePort,
signal: ac.signal,
onListen: onListen(listeningPromise),
onError: createOnErrorCb(ac),
});
await listeningPromise;
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 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 listeningPromise = deferred();
const server = Deno.serve({
handler: () => {
throw new Error("oops");
},
port: servePort,
signal: ac.signal,
onListen: onListen(listeningPromise),
onError: createOnErrorCb(ac),
});
await listeningPromise;
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 listeningPromise = deferred();
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(listeningPromise),
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 listeningPromise = deferred();
const server = Deno.serve({
handler: (_req) => new Response("ok"),
hostname: "localhost",
port: servePort,
signal: ac.signal,
onListen: onListen(listeningPromise),
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 = deferred();
let count = 0;
const server = Deno.serve({
async onListen({ port }: { port: number }) {
const client = Deno.createHttpClient({});
const res1 = await fetch(`http://localhost:${port}/`, { client });
assertEquals(await res1.text(), "hello world 1");
const res2 = await fetch(`http://localhost:${port}/`, { client });
assertEquals(await res2.text(), "hello world 2");
promise.resolve();
ac.abort();
client.close();
},
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 listeningPromise = deferred();
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 }) => listeningPromise.resolve(port),
onError: createOnErrorCb(ac),
});
try {
const port = await listeningPromise;
const client = Deno.createHttpClient({});
const resp = await fetch(`http://localhost:${port}/`, {
headers: { connection: "close" },
method: "POST",
body: '{"sus":true}',
client,
});
const text = await resp.text();
assertEquals(text, "ok");
client.close();
} 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 listeningPromise = deferred();
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 }) => listeningPromise.resolve(port),
});
try {
const port = await listeningPromise;
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 listeningPromise = deferred();
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 }) => listeningPromise.resolve(port),
});
try {
const port = await listeningPromise;
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 = deferred();
const server = Deno.serve({
async onListen({ port }) {
const res1 = await fetch(`http://localhost:${port}/`);
assertEquals((await res1.text()).length, 40 * 50_000);
promise.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());
}
Deno.test(
{ permissions: { net: true, run: true } },
async function httpServeCurlH2C() {
const ac = new AbortController();
const server = Deno.serve(
{ signal: ac.signal },
() => new Response("hello world!"),
);
assertEquals(
"hello world!",
await curlRequest(["http://localhost:8000/path"]),
);
assertEquals(
"hello world!",
await curlRequest(["http://localhost:8000/path", "--http2"]),
);
assertEquals(
"hello world!",
await curlRequest([
"http://localhost:8000/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 listeningPromise = deferred();
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(listeningPromise),
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;
},
);
Deno.test(
{ permissions: { net: true, run: true, read: true } },
async function httpsServeCurlH2C() {
const ac = new AbortController();
const server = Deno.serve(
{
signal: ac.signal,
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:8000/path", "-k"]),
);
assertEquals(
"hello world!",
await curlRequest(["https://localhost:8000/path", "-k", "--http2"]),
);
assertEquals(
"hello world!",
await curlRequest([
"https://localhost:8000/path",
"-k",
"--http2",
"--http2-prior-knowledge",
]),
);
ac.abort();
await server.finished;
},
);
async function curlRequest(args: string[]) {
const { success, stdout } = await new Deno.Command("curl", {
args,
stdout: "piped",
stderr: "null",
}).output();
assert(success);
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);
return [new TextDecoder().decode(stdout), new TextDecoder().decode(stderr)];
}