worker-tools / router

A router for Worker Runtimes such and Cloudflare Workers or Service Workers.
https://workers.tools/router
28 stars 4 forks source link

Handling uncaught HTTP exceptions #2

Open nounder opened 1 year ago

nounder commented 1 year ago

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/std@0.185.0/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 commented 1 year ago

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

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

// From:
// https://deno.com/manual@v1.33.2/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 commented 1 year ago

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/manual@v1.33.2/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 commented 1 year ago

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
      }
    }
  }