Skip to content

Oak leaking async ops on simple server test #454

Open

Description

@acrodrig

When trying to use oak to unit test some REST services, the server seems to leak async ops. Simplest code to reproduce below. Would love to know if I am doing something wrong (maybe explicit close connection?) or if this is a bug. Thanks much.

I run the test below using:

deno test -A server.test.ts 

Sample code:

import { assertEquals } from "https://deno.land/std/testing/asserts.ts";
import { Application, Router } from "https://deno.land/x/oak/mod.ts";

const PORT = 8080;

let listenPromise: Promise<void>;
const controller = new AbortController();

// Starts server and saves the returned promise into `listenPromise`
export const startServer = function(port: number): void {
    const app = new Application(), router = new Router();

    // Ping route returns "PONG""
    router.get("/ping", (ctx) => {
        ctx.response.body = "PONG";
        ctx.response.status = 200;
    });

    app.use(router.routes());
    app.use(router.allowedMethods());
    listenPromise = app.listen({ port: port, signal: controller.signal });
};

export const stopServer = async function(): Promise<void> {
    controller.abort();
    await listenPromise;
};

// starts small server defined above
startServer(PORT);

// BUG: THIS TEST CASE IS LEAKING ASYNC OPS, WHY?
Deno.test("test that I can connect to server", async function() {
    const url = "http://localhost:8080/ping";
    const response = await fetch(url);
    const text = await response.text();
    assertEquals(text, "PONG");
});

// Close the server on a test so that it runs at the end
Deno.test("close server", { sanitizeOps: false, sanitizeResources: false }, async function() {
    await stopServer();
});

The console output looks like this:

running 2 tests from file:///Users/andres/Code/RC/gateways/test/bug.test.ts
test test that I can connect to server ... FAILED (7ms)
test close server ... ok (1ms)

failures:

test that I can connect to server
AssertionError: Test case is leaking async ops.
Before:
  - dispatched: 2
  - completed: 1
After:
  - dispatched: 10
  - completed: 8
Ops:
  op_http_accept:
    Before:
      - dispatched: 0
      - completed: 0
    After:
      - dispatched: 2
      - completed: 1

Make sure to await all promises returned from Deno APIs before
finishing test case.
    at assert (deno:runtime/js/06_util.js:41:13)
    at asyncOpSanitizer (deno:runtime/js/40_testing.js:121:7)
    at async resourceSanitizer (deno:runtime/js/40_testing.js:137:7)
    at async Object.exitSanitizer [as fn] (deno:runtime/js/40_testing.js:169:9)
    at async runTest (deno:runtime/js/40_testing.js:427:7)
    at async Object.runTests (deno:runtime/js/40_testing.js:540:22)

failures:

	test that I can connect to server

test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out (29ms)

error: Test failed

Activity

  1. acrodrig commented on Jan 24, 2022

    @acrodrig
    Author

    I should add that if I replace URL http://localhost:8080/ping in the test case for an external URL such as https://filesamples.com/samples/code/json/sample2.json then the test no longer leaks, so presumably the leak is happening at the router.get "ping" function.

  2. cmorten commented on Aug 29, 2022

    @cmorten
    Contributor

    In this instance got a hunch it is your setup that's leaking based on the error.

    1. You have op_http_accept as 0 dispatched prior to the test. This indicates that although you have called startServer() prior to the test, Oak still hasn't managed to make it's first listener.accept() call yet.
    2. After the test we can see that 2 accept ops have been dispatched but only 1 has been completed. The completed one will be for the very first listener.accept() promise which will have completed when you made the fetch() call to the Oak server. The second accept op which we can see is dispatched but not completed is because under the hood Oak automatically looks to accept another connection but with no further requests (or stopping the server) this promise will never resolve.
    3. The simplest solution is likely to move the start and stop server functionality into the single test.

    E.g. the following adjustment to your test file doesn't throw:

    import { assertEquals } from "https://deno.land/std/testing/asserts.ts";
    import { Application, Router } from "https://deno.land/x/oak/mod.ts";
    
    const PORT = 8080;
    
    let listenPromise: Promise<void>;
    const controller = new AbortController();
    
    // Starts server and saves the returned promise into `listenPromise`
    export const startServer = function (port: number): void {
      const app = new Application(),
        router = new Router();
    
      // Ping route returns "PONG""
      router.get("/ping", (ctx) => {
        ctx.response.body = "PONG";
        ctx.response.status = 200;
      });
    
      app.use(router.routes());
      app.use(router.allowedMethods());
      listenPromise = app.listen({ port: port, signal: controller.signal });
    };
    
    export const stopServer = async function (): Promise<void> {
      controller.abort();
      await listenPromise;
    };
    
    Deno.test("test that I can connect to server", async function () {
      // starts small server defined above
      startServer(PORT);
    
      const url = "http://localhost:8080/ping";
      const response = await fetch(url);
      const text = await response.text();
      assertEquals(text, "PONG");
    
      await stopServer();
    });

    There may be a way to setup the tests to avoid having the startServer() and stopServer() calls in each of your tests. For this would likely need to make use of the "listen" event so you can await the first accept() call being made reliably.

    See https://github.com/oakserver/oak#opening-the-server

    E.g. in addition to your listenPromise have it's "listening" counterpart...

    const listeningPromise = new Promise<void>((resolve) => {
      app.addEventListener(
        "listen",
        () => {
          resolve();
        },
      );
    });
  3. gonzus commented on Jul 15, 2025

    @gonzus

    Hello. Apologies for replying to an issue after almost three years.

    I am seeing this same symptom with Deno tests that start up a server and then try to stop it: "http" was created during the test, but not cleaned up during the test. I copied your example above verbatim (without the "listening" counterpart) and ran it, same issue. This is with deno 2.4.1. Full output (running with --trace-leaks) below. Any ideas?

    ERRORS
    
    test that I can connect to server => ./src/server/tests/gonzo_test.ts:30:6
    error: Leaks detected:
      - "http" was created during the test, but not cleaned up during the test. Close the resource before the end of the test.
      - An async call to op_http_wait was started in this test, but never completed. The operation was started here:
        at op_http_wait (ext:core/00_infra.js:258:13)
        at ext:deno_http/00_serve.ts:761:26
        at eventLoopTick (ext:core/01_core.js:178:7)
    
     FAILURES
    
    test that I can connect to server => ./src/server/tests/gonzo_test.ts:30:6
    
  4. gonzus commented on Jul 15, 2025

    @gonzus

    Also (unrelated), I have been seeing this warning for a couple of weeks / deno versions now: oak send() does not work under Node.js. There does not seem to be a way to turn that off. Is this intended, or a regression? I am not using send() anywhere.

  5. thehenrytsai commented on Jul 16, 2025

    @thehenrytsai

    @gonzus, I ran into the exact same problem after upgrading to Deno v.2.4.1 with my tests.

    After various failed attempts, I upgraded Oak from v17.1.4 to v17.1.5 which made the errors go away!

  6. gonzus commented on Jul 17, 2025

    @gonzus

    Thanks for the hint @thehenrytsai -- upgrading Oak seems to have fixed the test for me, but with some minor changes. I am pasting my full test below, for posterity.

    import { STATUS_CODE } from "jsr:@std/http/status";
    import { Application, Router, Context } from "@oak/oak";
    import { assertEquals } from "jsr:@std/assert";
    
    const PORT = 8182;
    
    const controller = new AbortController();
    
    async function startServer(port: number): Promise<void> {
      const router = new Router();
    
      router.get("/ping", (ctx: Context) => {
        ctx.response.body = "PONG";
        ctx.response.status = STATUS_CODE.OK;
      });
    
      const app = new Application();
      app.use(router.routes());
      app.use(router.allowedMethods());
      await app.listen({ port, signal: controller.signal });
    }
    
    function stopServer(){
      console.log("STOPPPING SERVER");
      controller.abort();
    }
    
    Deno.test("test that I can connect to server", async () => {
      // NOTE: if this is called with await, it will freeze
      startServer(PORT);
      console.log("STARTED SERVER");
    
      console.log("SENDING PING");
      const url = `http://localhost:${PORT}/ping`;
      const response = await fetch(url);
      const text = await response.text();
      assertEquals(response.status, STATUS_CODE.OK);
      assertEquals(text, "PONG");
      console.log("GOT PONG");
    
      console.log("REQUESTING STOP SERVER");
      stopServer();
      console.log("STOPPED SERVER");
    });

    which produces the following output:

    $ deno test -A src/server/tests/gonzo_test.ts
    running 1 test from ./src/server/tests/gonzo_test.ts
    test that I can connect to server ...
    ------- output -------
    STARTED SERVER
    SENDING PING
    GOT PONG
    REQUESTING STOP SERVER
    STOPPPING SERVER
    STOPPED SERVER
    ----- output end -----
    test that I can connect to server ... ok (6ms)
    
    ok | 1 passed | 0 failed (8ms)
    
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions