DEV Community

Alex Tsang
Alex Tsang

Posted on

Avoid leaking resources in Deno tests

Recently I have been using Deno to implement a simple web API to play with the new TypeScript runtime. In order to test the APIs, today I wrote a test, which is similar to the following:

import {
  assertStrictEq,
} from "https://deno.land/std@v0.51.0/testing/asserts.ts";

import {
  Application,
} from "https://deno.land/x/oak@v5.0.0/mod.ts";

Deno.test("server with a simple middleware", async () => {
  const app = new Application();
  const controller  = new AbortController();
  const { signal } = controller;

  app.use(async (ctx) => {
    ctx.response.body = 'Hello world.';
  });

  const listenPromise = app.listen({ port: 8000, signal });

  const response = await fetch("http://127.0.0.1:8000/");
  assertStrictEq(response.ok, true);

  controller.abort();

  await listenPromise;
});

The test is simple:

  • Create a new application.
  • Mount a middleware on the application so that the application will respond with "Hello world." when a request arrives.
  • Start the application and listen on the specified port (i.e. 8000).
  • Send a request to the application and test whether the response status code is in expected range (response.ok means the status is in the range 200 - 299).
  • Stop the application.

This test should pass... Except when it didn't:

> deno test --allow-net
running 1 tests
test server with a simple middleware ... FAILED (38ms)

failures:

server with a simple middleware
AssertionError: Test case is leaking resources.
Before: {
  "0": "stdin",
  "1": "stdout",
  "2": "stderr"
}
After: {
  "0": "stdin",
  "1": "stdout",
  "2": "stderr",
  "5": "httpBody"
}

Make sure to close all open resource handles returned from Deno APIs before
finishing test case.
    at Object.assert ($deno$/util.ts:33:11)
    at Object.resourceSanitizer [as fn] ($deno$/testing.ts:81:5)
    at async TestApi.[Symbol.asyncIterator] ($deno$/testing.ts:264:11)
    at async Object.runTests ($deno$/testing.ts:346:20)

failures:

        server with a simple middleware

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

At first I didn't understand which part of the test was leaking resources. After some experiments, I found that the root cause was that I didn't read the response body obtained from the fetch() function call.

After calling await fetch() to obtain the response, one can call response.text() (or response.json()) to read the response stream and parse it as JSON data (or plain text) (see the document on MDN for details). Without reading the response stream (which is expected to be read, I guess), Deno would think the test is leaking resources.

It is very simple to fix the problem. When using Deno 1.0.3 (the latest version at the time of writing), one can call cancel() on the response stream to say that the data is not needed anymore. I updated the test:

import {
  assertStrictEq,
} from "https://deno.land/std@v0.51.0/testing/asserts.ts";

import {
  Application,
} from "https://deno.land/x/oak@v5.0.0/mod.ts";

Deno.test("server with a simple middleware", async () => {
  const app = new Application();
  const controller  = new AbortController();
  const { signal } = controller;

  app.use(async (ctx) => {
    ctx.response.body = 'Hello world.';
  });

  const listenPromise = app.listen({ port: 8000, signal });

  const response = await fetch("http://127.0.0.1:8000/");
  assertStrictEq(response.ok, true);
  // The response body is not needed.
  await response.body?.cancel();

  controller.abort();

  await listenPromise;
});

Notice that line after checking the response status:

await response.body?.cancel();

(See Optional chaining if you wonder what does ?. mean.)

Finally the test passed:

> deno test --allow-net
running 1 tests
test server with a simple middleware ... ok (89ms)

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

If you are using Deno 1.0.2 (or previous versions), the cancel() function is not implemented:

> deno test --allow-net
running 1 tests
test server with a simple middleware ... FAILED (71ms)

failures:

server with a simple middleware
Error: not implemented
    at Object.notImplemented ($deno$/util.ts:64:9)
    at Body.cancel ($deno$/web/fetch.ts:234:12)

You can still read the response body and throw it away immediately:

await response.text();

In fact, the same leaking resources problem has been asked and addressed around a month ago, you can see the issue #4735 on Deno's code repository for more details.

Some useful links:

Top comments (2)

Collapse
 
za profile image
Zach

Just ran into this, thanks!

Collapse
 
michaeltempest profile image
Michael Tempest

Thank you for posting this, it would have took me a long time to overcome this issue.