router icon indicating copy to clipboard operation
router copied to clipboard

Handling uncaught HTTP exceptions

Open nounder opened this issue 1 year ago • 3 comments

With Server-sent endpoints it is often the case that connection is closed before Deno flushes body stream. When that happens, following error is thrown:

Uncaught Http: connection closed before message completed
      await requestEvent.respondWith(response);
      ^
    at Object.respondWith (ext:deno_http/01_http.js:336:25)
    at eventLoopTick (ext:core/01_core.js:188:13)
    at async Server.#respond (https://deno.land/[email protected]/http/server.ts:311:7)

I tried to try/catch handler and listen to router 'error' handler but it looks like the error is thrown outside the context of the router.

Is it somehow possible to catch this error and conditionally silence it?

nounder avatar May 15 '23 18:05 nounder

I went ahead with handling Deno.RequestEvent directly like so:

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

// From:
// https://deno.com/[email protected]/runtime/http_server_apis#responding-with-a-response
async function handle(conn: Deno.Conn) {
  const httpConn = Deno.serveHttp(conn)

  for await (const requestEvent of httpConn) {
    try {
      await requestEvent.respondWith(
        await RootRouter.fetch(requestEvent.request)
      )
    } catch (err) {
      console.warn(err)
    }
  }
}

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

Although there is try/catch around respondWith, the server still crashes:

error: Uncaught Http: connection closed before message completed
        await requestEvent.respondWith(
        ^
    at Object.respondWith (ext:deno_http/01_http.js:328:21)
    at eventLoopTick (ext:core/01_core.js:188:13)
    at async handle (file:///MyOwnCode/main.ts:421:9)

nounder avatar May 15 '23 21:05 nounder

Here's standalone example:

import * as shed from "https://raw.githubusercontent.com/worker-tools/shed/master/index.ts"

export const RootRouter = new shed.WorkerRouter()

RootRouter.get("/", () => {
  const stream = new TransformStream()
  const writer = stream.writable.getWriter()

  setInterval(() => {
    writer.write(new TextEncoder().encode("ping\n"))
  }, 100)

  return new shed.StreamResponse(stream.readable, {
    headers: {
      "Content-Type": "text/event-stream",
    },
  })
})

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

// From:
// https://deno.com/[email protected]/runtime/http_server_apis#responding-with-a-response
async function handle(conn: Deno.Conn) {
  const httpConn = Deno.serveHttp(conn)

  for await (const requestEvent of httpConn) {
    try {
      await requestEvent.respondWith(RootRouter.fetch(requestEvent.request))
    } catch (err) {
      console.log("Connection loop error")

      console.warn(err)

      break
    }
  }
}

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

Now go to localhost:8080 and refresh a page. Process will crash:

error: Uncaught Http: connection closed before message completed
      await requestEvent.respondWith(RootRouter.fetch(requestEvent.request))
      ^
    at Object.respondWith (ext:deno_http/01_http.js:328:21)
    at eventLoopTick (ext:core/01_core.js:188:13)
    at async handle (file:///[...]/shed_break.ts:29:7)

nounder avatar May 16 '23 06:05 nounder

Using server_sent_event.ts from std solves the issue. See https://github.com/denoland/deno/issues/19143#issuecomment-1549141025 for more.

One can filter SSE closes with following code:

  async function handle(conn: Deno.Conn) {
    const httpConn = Deno.serveHttp(conn)

    for await (const requestEvent of httpConn) {
      const responsePromise = RootRouter.fetch(requestEvent.request)
      try {
        await requestEvent.respondWith(responsePromise)
      } catch (err) {
        const res = await responsePromise

        if (
          res.headers.get("content-type") === "text/event-stream" &&
          err.message === "connection closed before message completed"
        ) {
          continue
        }

        throw err
      }
    }
  }

nounder avatar May 16 '23 07:05 nounder