hapijs / hapijs/hapi

Error-tracking software doesn't catch hapi application errors if an incoming request gets prematurely closed

Open
#4,567 2 comments 1 reaction 0 assignees View on GitHub
bug
Dominant language
JavaScript
Stars
14.8k
Forks
1.4k
Avg merge
22d 3h
Merged PRs (30d)
1

Description

### Runtime

Node.js

### Runtime version

v25.7.0

### Module version

v21.4.7

### Last module version without issue

_No response_

### Used with

Sentry, Rollbar, DataDog

### Any other relevant information

Sentry, Rollbar, and DataDog support tracking errors in hapi applications, relying on either hapi's `request` `error` event or on `onPreResponse` lifecycle method. See:
- https://github.com/getsentry/sentry-javascript/blob/master/packages/node/src/integrations/tracing/hapi/index.ts#L67
- https://docs.rollbar.com/docs/nodejs#using-hapi
- https://github.com/DataDog/dd-trace-js/blob/master/packages/datadog-instrumentations/src/hapi.js#L33

### What are you trying to achieve or the steps to reproduce?

Here is example code that reproduces an issue where an error thrown in a handler bypasses both the `request` `error` event and `onPreResponse` lifecycle method calls.

```js
import Hapi from "@hapi/hapi";
import { setTimeout as sleep } from "node:timers/promises"

const server = Hapi.server({
port: 3000,
host: "localhost"
});

server.events.on({ name: "request", channels: ["error"] }, (request, event, tags) => {
console.log("[server] on request error"); // NOT called
})

server.ext("onPreResponse", async (request, h) => {
console.log("[server] onPreResponse"); // NOT called
return request;
});

server.route({
method: "GET",
path: "/sleep/{seconds}/then-throw-error",
handler: async (request, h) => {
const { seconds } = request.params;
for (let i = 0; i < seconds; i++) {
console.log("[handler] Handling a request... (sleep %s)", i + 1);
await sleep(1_000);
}
console.log("[handler] Throwing error");
throw new Error("Boom!");
}
});

await server.start();
console.log("[server] Running on %s", server.info.uri);

try {
console.log("[fetch] Making a request");
await fetch("http://localhost:3000/sleep/5/then-throw-error", { signal: AbortSignal.timeout(1_000) });
} catch (error) {
if (error.name === "TimeoutError") {
console.log("[fetch] Stopped waiting for the response");
} else {
console.log(error);
}
}
```

### What was the result you got?

Here is the output produced by the example code:

```plain
[server] Running on http://localhost:3000
[fetch] Making a request
[handler] Handling a request... (sleep 1)
[fetch] Stopped waiting for the response
[handler] Handling a request... (sleep 2)
[handler] Handling a request... (sleep 3)
[handler] Handling a request... (sleep 4)
[handler] Handling a request... (sleep 5)
[handler] Throwing error
```

Neither `server.events.on({ name: "request", channels: ["error"] }, fn)` nor `server.ext("onPreResponse", fn)` callback functions are called if the incoming request gets prematurely closed. However, if we increase the request timeout, e.g., `AbortSignal.timeout(10_000)`, both callback functions will get called.

### What result did you expect?

I'm not sure if the shown hapi behavior is a bug or if it works as intended, especially because the `onPreResponse ` documentation [mentions](https://hapi.dev/api/?v=21.4.3#request-lifecycle) that it is “always called, _unless the request is aborted_.” Nevertheless, it's evident that there is existing error-tracking software that seems to be expecting to capture _all_ errors using the approach. The fact that some errors are not getting captured is an issue. I would like to start a discussion to figure out a way forward on how to improve error tracking around aborted requests.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.