summaryrefslogtreecommitdiff
path: root/cli/tests
diff options
context:
space:
mode:
Diffstat (limited to 'cli/tests')
-rw-r--r--cli/tests/unit/serve_test.ts244
1 files changed, 238 insertions, 6 deletions
diff --git a/cli/tests/unit/serve_test.ts b/cli/tests/unit/serve_test.ts
index d1ac82696..3f58903a8 100644
--- a/cli/tests/unit/serve_test.ts
+++ b/cli/tests/unit/serve_test.ts
@@ -41,27 +41,257 @@ function onListen<T>(
};
}
-Deno.test(async function httpServerShutsDownPortBeforeResolving() {
+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: (_req) => new Response("ok"),
+ handler,
port: servePort,
signal: ac.signal,
onListen: onListen(listeningPromise),
});
await listeningPromise;
- assertThrows(() => Deno.listen({ port: servePort }));
+ return {
+ finished: server.finished,
+ abort() {
+ ac.abort();
+ },
+ async shutdown() {
+ await server.shutdown();
+ },
+ };
+}
- ac.abort();
- await server.finished;
+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() {
@@ -2459,7 +2689,9 @@ for (const url of ["text", "file", "stream"]) {
// Give it a few milliseconds for the serve machinery to work
await new Promise((r) => setTimeout(r, 10));
- ac.abort();
+ // 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;
},
});