Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handling uncaught HTTP exceptions #2

Open
nounder opened this issue May 15, 2023 · 3 comments
Open

Handling uncaught HTTP exceptions #2

nounder opened this issue May 15, 2023 · 3 comments

Comments

@nounder
Copy link

nounder commented May 15, 2023

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
Copy link
Author

nounder commented May 15, 2023

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
Copy link
Author

nounder commented May 16, 2023

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
Copy link
Author

nounder commented May 16, 2023

Using server_sent_event.ts from std solves the issue. See denoland/deno#19143 (comment) 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
      }
    }
  }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant