javascripttypescriptdenooak

How to correctly close a listening port in oak?


I'm trying to close the port manually (in somewhere else) but not sure the way to do it.

A smallest example:

import { Application, Router } from 'jsr:@oak/oak'
const app = new Application()
const router = new Router()

app.use(router.routes())
app.use(router.allowedMethods())

router.get('/', (ctx) => {
  ctx.response.body = JSON.stringify({ message: 'Hello, Oak!' })
})

const ac = new AbortController()
const promise = app.listen({ port: 8000, signal: ac.signal })

async function close() {
  ac.abort()
  await promise
  // never reached
  console.log('Server closed')
}

await close()

Solution

  • The readme file in the Oak repository includes a code example (closing the server) which shows the equivalent of the code in your question, but — at the time I write this answer — there appears to be a bug in Oak (v17.1.6) that prevents the promise returned by the Application.prototype.listen method from fulfilling. You can see more info and subscribe for updates at the following GitHub PR:

    oakserver/oak#685 — fix: make Application listen promise resolve after aborted

    In the meantime, you can create your own promise… and then resolve it in a callback function that is provided to the Application.prototype.addEventListener method like this:

    example.ts:

    import { Application } from "jsr:@oak/oak@17.1.6";
    
    import type {
      ApplicationCloseEvent,
      ApplicationListenEvent,
    } from "jsr:@oak/oak@17.1.6/application";
    
    const app = new Application();
    app.use((ctx) => ctx.response.body = "hello world");
    
    const serverListening = Promise.withResolvers<ApplicationListenEvent>();
    app.addEventListener("listen", serverListening.resolve, { once: true });
    
    const serverClosed = Promise.withResolvers<ApplicationCloseEvent>();
    app.addEventListener("close", serverClosed.resolve, { once: true });
    
    const controller = new AbortController();
    app.listen({ hostname: "localhost", port: 8000, signal: controller.signal });
    
    const listenEvent = await serverListening.promise;
    console.log(`Server listening on port ${listenEvent.port}`);
    
    controller.abort();
    await serverClosed.promise;
    console.log("Server closed");
    
    

    Running with Deno (v2.4.5):

    > deno run --no-config --allow-net=localhost:8000 example.ts
    Server listening on port 8000
    Server closed
    
    

    Here's a reproducible test module based on the code in your question:

    test.ts:

    import { assert } from "jsr:@std/assert@1.0.14/assert";
    import { assertRejects } from "jsr:@std/assert@1.0.14/rejects";
    
    import {
      Application,
      type ListenOptionsBase,
      Router,
    } from "jsr:@oak/oak@17.1.6";
    
    import type {
      ApplicationCloseEvent,
      ApplicationListenEvent,
    } from "jsr:@oak/oak@17.1.6/application";
    
    Deno.test("oak server", async (t) => {
      const app = new Application();
      const router = new Router();
    
      app.use(router.routes());
      app.use(router.allowedMethods());
    
      const message = "Hello, Oak!";
    
      router.get("/", (ctx) => {
        ctx.response.body = JSON.stringify({ message });
      });
    
      const serverListening = Promise.withResolvers<ApplicationListenEvent>();
      app.addEventListener("listen", serverListening.resolve, { once: true });
    
      const serverClosed = Promise.withResolvers<ApplicationCloseEvent>();
      app.addEventListener("close", serverClosed.resolve, { once: true });
    
      // let serverStopped: Promise<void>;
      const ac = new AbortController();
    
      const listenOptions: ListenOptionsBase = {
        hostname: "localhost",
        port: 8000,
        signal: ac.signal,
      };
    
      await t.step("starts", async () => {
        // Ref: https://github.com/oakserver/oak/pull/685
        /* serverStopped = */ app.listen(listenOptions);
        await serverListening.promise;
      });
    
      const assertExpectedResponse: () => Promise<void> = async () => {
        const url = `http://${listenOptions.hostname}:${listenOptions.port}`;
        const response = await fetch(url);
        assert(response.ok);
        const actualMessage = (await response.json()).message;
        assert(actualMessage === message);
      };
    
      await t.step("responds", assertExpectedResponse);
    
      await t.step("stops", async () => {
        ac.abort();
        // await serverStopped;
        await serverClosed.promise;
      });
    
      await t.step("no longer responds", async () => {
        await assertRejects(assertExpectedResponse, TypeError);
      });
    });
    
    

    Running with Deno's test runner:

    > deno --version
    deno 2.4.5 (stable, release, aarch64-apple-darwin)
    v8 13.7.152.14-rusty
    typescript 5.8.3
    
    > deno test --no-config --allow-net=localhost:8000 test.ts
    running 1 test from ./test.ts
    oak server ...
      starts ... ok (2ms)
      responds ... ok (3ms)
      stops ... ok (0ms)
      no longer responds ... ok (1ms)
    oak server ... ok (7ms)
    
    ok | 1 passed (4 steps) | 0 failed (8ms)