Search code examples
serverdenoabort

How to abort `Deno.serve` via request


How to abort Deno.serve via request right after the respective response was sent?

My current workaround is a 1s sleep before aborting the AbortController. I've tried queueMicrotask, but it seems like the response is not sent via the main thread.

Here is my workaround:

//example.ts
//deno run --allow-net=127.0.0.1 example.ts

const port = 3000;
const hostname = "127.0.0.1";
const ac = new AbortController();
const signal = ac.signal;

let isShuttingDown = false;
const server = Deno.serve(
  { port, hostname, signal },
  (req: Request, _info: Deno.ServeHandlerInfo) => {
    if (isShuttingDown) {
      return new Response("Server is shutting down!", { status: 503 });
    }
    const url = new URL(req.url);
    if (
      url.pathname === "/shutdown"
    ) {
      isShuttingDown = true;
      // queueMicrotask(()=>{ //does not run after response is sent
      //   ac.abort();
      // });
      setTimeout(() => {
        ac.abort();
      }, 1000); //give client time to receive response
      return new Response(null, { status: 202 });
    }
    return new Response("hello");
  },
);
await server.finished;
console.log("server stopped");

Is there a better way than waiting with a long enough timeout?


Solution

  • In Deno v1.38, an unstable method shtudown was added to the class Deno.HttpServer to facilitate graceful shutdown.

    shutdown(): Promise<void>
    

    Gracefully close the server. No more new connections will be accepted, while pending requests will be allowed to finish.

    I haven't yet reviewed the source code implementation (so maybe I'm missing something) but using it inside a server handler function currently still appears to require a delay. Perhaps the implementation prevents any new responses from being sent immediately after invocation — the documentation does not make this clear.

    In short, you can gracefully shutdown the server in your request handler callback function just before returning the response, like this:

    function handler() {
      queueMicrotask(httpServer.shutdown);
      return new Response(/* … */);
    }
    

    Here's a complete reproducible example:

    server.ts:

    /// <reference lib="deno.unstable" />
    
    function delay(ms: number): Promise<void> {
      return new Promise((res) => setTimeout(res, ms));
    }
    
    function createPlainTextResponse(
      text: string,
      init: ResponseInit = {},
    ): Response {
      const { headers: headersInit, ...rest } = init;
      const headers = new Headers(headersInit);
      headers.set("Content-Type", "text/plain; charset=utf-8");
      return new Response(text, { ...rest, headers });
    }
    
    function handleNotFound(): Response {
      return createPlainTextResponse("Not Found", { status: 404 });
    }
    
    const routeHandlers: Record<string, Deno.ServeHandler> = {
      "/hello": () => createPlainTextResponse("Hello world"),
      "/shutdown": () => {
        queueMicrotask(httpServer.shutdown);
        return createPlainTextResponse("Server is shutting down", { status: 202 });
      },
    };
    
    const handleRequest: Deno.ServeHandler = (request, info) => {
      const url = new URL(request.url);
      const handler = routeHandlers[url.pathname] ?? handleNotFound;
      return handler(request, info);
    };
    
    function printStartupMessage({ hostname, port, secure }: {
      hostname: string;
      port: number;
      secure?: boolean;
    }): void {
      if (!hostname || hostname === "0.0.0.0") hostname = "localhost";
      const address =
        new URL(`http${secure ? "s" : ""}://${hostname}:${port}/`).href;
      console.log(`Listening at ${address}`);
      console.log("Use ctrl+c to stop");
    }
    
    async function logInfoAndSendTestRequests(
      opts: Parameters<NonNullable<Deno.ServeOptions["onListen"]>>[0],
    ): Promise<void> {
      printStartupMessage(opts);
      const origin = `http://${opts.hostname}:${opts.port}`;
    
      console.log("Client started");
    
      for (const pathname of ["/hello", "/oops", "/shutdown", "/hello"]) {
        try {
          await delay(250);
          const response = await fetch(`${origin}${pathname}`);
          const text = await response.text();
          console.log(pathname, response.status, text);
        } catch (cause) {
          console.error("Caught client exception:", cause);
        }
      }
    
      console.log("Client stopped");
    }
    
    const opts = {
      hostname: "localhost",
      onListen: logInfoAndSendTestRequests,
      port: 3000,
    } satisfies Deno.ServeOptions;
    
    const httpServer = Deno.serve(opts, handleRequest);
    await httpServer.finished;
    console.log("Server stopped");
    
    

    Terminal:

    % deno --version
    deno 1.38.3 (release, aarch64-apple-darwin)
    v8 12.0.267.1
    typescript 5.2.2
    
    % deno run --unstable --allow-net=localhost:3000 server.ts
    Listening at http://localhost:3000/
    Use ctrl+c to stop
    Client started
    /hello 200 Hello world
    /oops 404 Not Found
    Server stopped
    /shutdown 202 Server is shutting down
    Caught client exception: TypeError: error sending request for url (http://localhost:3000/hello): error trying to connect: tcp connect error: Connection refused (os error 61)
        at async mainFetch (ext:deno_fetch/26_fetch.js:277:12)
        at async fetch (ext:deno_fetch/26_fetch.js:504:7)
        at async Object.logInfoAndSendTestRequests [as onListen] (file:///Users/deno/so-77547677/server.ts:58:24)
    Client stopped
    
    % echo $?
    0