oakserver / oak

A middleware framework for handling HTTP with Deno, Node, Bun and Cloudflare Workers 🐿️ 🦕
https://oakserver.org
MIT License
5.18k stars 235 forks source link

Can't close the server after upgrading a connection to Websocket #268

Open Andrepuel opened 3 years ago

Andrepuel commented 3 years ago

Code to reproduce the issue:

import { Application, Context } from 'https://deno.land/x/oak@v6.4.1/mod.ts';

const app = new Application();

app.use(async (ctx) => {
    console.log('upgrading');
    const socket = await ctx.upgrade();
    console.log('upgraded');
});

const controller = new AbortController();

const listening = app.listen({ port: 8000, signal: controller.signal });
await new Promise((ok) => setTimeout(ok, 100));

const ws = new WebSocket("ws://127.0.0.1:8000");
await new Promise<void>((ok, err) => {
    ws.onopen = () => ok();
    ws.onerror = (e) => {
      console.error(e);
      ok();
    }
});
await new Promise((ok) => setTimeout(ok, 500));
ws.close();

await new Promise((ok) => setTimeout(ok, 500));
controller.abort();
console.log('aborting');

await Promise.all([
    listening,
    new Promise((_, err) => setTimeout(() => err(new Error("stuck")), 1000))
]);
Andrepuel commented 3 years ago

Looks like the listen() promise is stuck in the Server async iterator. Even after an connection is upgraded to WebSocket, it will still be handled by the iterator, waiting for a deferred promise called done.

Andrepuel commented 3 years ago

By doing the following workaround, I've managed to end the listening promise:

import { Application, Context } from 'https://deno.land/x/oak@v6.4.1/mod.ts';

const app = new Application();

app.use(async (ctx) => {
    console.log('upgrading');
    const socket = await ctx.upgrade();
    // <---------------------------------            Workaround, force untracking this connection
    ctx.request.serverRequest.done.resolve(new Error('force untrack'));
    console.log('upgraded');
});

const controller = new AbortController();

const listening = app.listen({ port: 8000, signal: controller.signal });
await new Promise((ok) => setTimeout(ok, 100));

const ws = new WebSocket("ws://127.0.0.1:8000");
await new Promise<void>((ok, err) => {
    ws.onopen = () => ok();
    ws.onerror = (e) => {
    console.error(e);
    ok();
    }
});
await new Promise((ok) => setTimeout(ok, 500));
ws.close();

await new Promise((ok) => setTimeout(ok, 500));
controller.abort();
console.log('aborting');

await Promise.all([
    listening,
    new Promise((_, err) => setTimeout(() => err(new Error("stuck")), 1000))
]);

However, the main process is still stuck. There is an async op that is leaking. By debugging the async op's I've figured out that a op named "ws next event" never returns. Even after the websocket is closed.

Andrepuel commented 3 years ago

The second problem (stuck after listening finishes) is happening on the client side, not related to OAK.

https://github.com/denoland/deno/issues/7457