deno: Process exits even when catching: Uncaught (in promise) Http: connection closed before message completed

deno 1.12.2 (release, x86_64-apple-darwin) v8 9.2.230.14 typescript 4.3.5

Refreshing the browser quickly multiple times against a simple local http server can produce a situation where a promise is rejected that cannot be caught, and causes process exits. This is not good when trying to write a stable http server.

for await (const { request, respondWith } of httpConn) {
  try {
    const res = await computeResponse();
    await respondWith(res)
  } catch (e) {
    console.error('Error in respondWith', e);
  }
}

yields:

Error in respondWith Http: connection closed before message completed
    at deno:core/01_core.js:106:46
    at unwrapOpResult (deno:core/01_core.js:126:13)
    at async respondWith (deno:extensions/http/01_http.js:183:27)
    at async handle (file:///myfile.ts:87:13)
Http: connection closed before message completed
    at deno:core/01_core.js:106:46
    at unwrapOpResult (deno:core/01_core.js:126:13)
    at async respondWith (deno:extensions/http/01_http.js:183:27)
    at async handle (file:///myfile.ts:87:13)
error: Uncaught (in promise) Http: connection closed before message completed
            await respondWith(res);
            ^
    at deno:core/01_core.js:106:46
    at unwrapOpResult (deno:core/01_core.js:126:13)
    at async respondWith (deno:extensions/http/01_http.js:183:27)
    at async handle (file:///myfile.ts:87:13)
<process exit>

Tried a .catch(), now none of my code appears in this stacktrace at all!

for await (const { request, respondWith } of httpConn) {
  const res = await computeResponse();
  respondWith(res).catch(e => console.log(`Error in respondWith`, e));
}

yields:

Http: connection closed before message completed
    at deno:core/01_core.js:106:46
    at unwrapOpResult (deno:core/01_core.js:126:13)
    at async respondWith (deno:extensions/http/01_http.js:183:27)
error: Uncaught (in promise) Http: connection closed before message completed
    at deno:core/01_core.js:106:46
    at unwrapOpResult (deno:core/01_core.js:126:13)
    at async respondWith (deno:extensions/http/01_http.js:183:27)
<process exit>

I’m at a loss for what to do here, especially since unhandled promise rejections cannot currently be caught globally (#7013)

Found older #10128 and #10380, but the code seems to have substantially changed since then, based on the stacktraces.

About this issue

  • Original URL
  • State: closed
  • Created 3 years ago
  • Comments: 26 (21 by maintainers)

Most upvoted comments

@johnspurlock so with help from @lucacasonato and @ry we’ve debugged the problem.

Firstly, there’s a problem with stack trace that doesn’t give useful information, the best I could get was:

error: Uncaught (in promise) Http: connection closed before message completed
            const res = await computeResponse();
                                             ^
    at deno:core/01_core.js:106:46
    at unwrapOpResult (deno:core/01_core.js:126:13)
    at async respondWith (deno:ext/http/01_http.js:183:27)
    at async file:///Users/biwanczuk/dev/deno/cli/tests/unit/http_test.ts:835:46

which points that there’s unresolved promise in the computeResponse function.

There’s also problem in your code in that you don’t await writer.write and writer.close which both are promises and that was the root cause of the uncaught promise rejecetion.

If you rewrite your code like so:

async function writeResponse(writer): Promise<Response> {
    try {
        await writer.write(new TextEncoder().encode('written to the writable side of a TransformStream'));
    } catch (e) {
        console.log(`Error in writer.write`, e);
    }
    await writer.close();
    
}

async function handle(conn: Deno.Conn) {
    const httpConn = Deno.serveHttp(conn);
    for await (const { request, respondWith } of httpConn) {
        const { readable, writable } = new TransformStream<Uint8Array>();
        const writer = writable.getWriter();
        writeResponse(writer).catch((e) => { console.error(`uncaught in writeResponse ${request.url}`, e); });
        const res = new Response(readable);
        const res = await computeResponse();
        respondWith(res).catch((e) => { console.error(`uncaught in respondWith ${request.url}`, e); });
    }
}

const server = Deno.listen({ port: 3002 });

for await (const conn of server) {
    handle(conn);
}

the problem goes away. (Writing of response needs to be done in an async IIFE because of WHATWG streams backpressure).

There’s also a bug in internal code that doesn’t properly clean up one resource that I’m working on a fix for, but I’m afraid it might be released in 1.13.2 in a week as there’s some refactors I need to do before that can be fixed. This should be released in 1.13.1 after all.

Thanks for providing this test case - this is quite an esoteric bug but I’m glad we got to the bottom of it.

Thanks @johnspurlock I’m getting the panic now. I will try to fix it for tomorrow.

Thanks for reports @johnspurlock @GJZwiers, I saw this issue today and it is something we’ll look into this week to release fixes for 1.13.1 on Monday. I’ll get back to you once I debug the problem, but from the initial investigation it seems to be an error that should be handled internally - looks like client is closing the connection before server writes full response.

If I reload the browser before the response from the server arrives I get the error too, how can I catch this error so that the application does not crash?

Example:

const sleep = (ms: number) => {
  return new Promise(resolve => setTimeout(resolve, ms))
}

const server = Deno.listen({ port: 8000 });

console.log("http://localhost:8000/");

try {
  for await (const conn of server) {
    (async () => {
      const httpConn = Deno.serveHttp(conn);
      for await (const { respondWith } of httpConn) {
        // Placeholder for some calculations or requests from other services
        // This makes the difference
        await sleep(200);
        respondWith(new Response("hello world", {
          status: 200,
        }));
      }
    })();
  }
} catch (error) {
  // Note: This is not called for the error: "Http: connection closed before message completed"
  console.warn("Error", error);
}

Start this with deno run --allow-net app.ts

If I remove await sleep(200); it also happens when I hold down the F5 key of the browser so that many requests are generated in a short time.

There’s also a bug in internal code that doesn’t properly clean up one resource that I’m working on a fix for

What resource is this @bartlomieju? I’ve noticed that if code throws prior to calling respondWith() successfully you are left with a hanging responseSender resource even if you have closed the underlying listener and httpConn - just in case it’s the same thing and not to worry about raising an issue 🙃

Yes, it’s the same one, it’s actually called responseBody.