hapijs / hapijs/hapi

The data property of a Boom error gets mangled when the error comes from a validator

Open
#4,582 3 comments 0 reactions 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

v24.12.0

### Module version

v21.4.9

### Last module version without issue

v20.3.0

### Used with

hapi application

### Any other relevant information

I believe https://github.com/hapijs/hapi/pull/4350 introduced an edge case issue.

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

Here is a distilled test to showcase the issue.

```mjs
import Boom from "@hapi/boom"; // 10.0.1
import Hapi from "@hapi/hapi"; // 21.4.9
import Joi from "joi"; // 18.2.3

import assert from "node:assert";
import { afterEach, beforeEach, describe, it } from "node:test";

describe("error mangling showcase", () => {
let server;
const myError = Boom.badRequest("My message", { my: "data" });

beforeEach(async () => {
// Initialize Hapi
server = Hapi.server();
await server.initialize();

// Set up a basic global error response formatter to expose the error's data property
server.ext("onPreResponse", (request, h) => {
const { response } = request;

if (response.isBoom) {
const error = response;
error.output.payload = {
error: {
message: error.message,
data: error.data,
},
};
}

return h.continue;
});
});

afterEach(async () => {
await server.stop();
});

it("throws myError from handler", async () => {
server.route({
method: "GET",
path: "/",
handler: function () {
throw myError;
},
});

const res = await server.inject({
method: "get",
url: "/",
});

// This assertion passes regarless of Hapi version
assert.deepStrictEqual(res.result, {
error: {
message: "My message",
data: { my: "data" },
},
});
});

it("throws myError from validation schema", async () => {
server.route({
method: "GET",
path: "/",
handler: function () {
return "Hello world!";
},
options: {
validate: {
query: Joi.object({
dummy: Joi.required().error(myError),
}),
},
},
});

const res = await server.inject({
method: "get",
url: "/",
});

// This assertion fails on Hapi v21 and onward
assert.deepStrictEqual(res.result, {
error: {
message: "My message",
// the data is { defaultError: myError } starting from Hapi v21
data: { my: "data" },
},
});
});
});
```

### What was the result you got?

```console
$ node --test example.mjs
▶ issue showcase
✔ throws myError from handler (23.230669ms)
✖ throws myError from validation schema (11.012993ms)
✖ issue showcase (35.352659ms)
ℹ tests 2
ℹ suites 1
ℹ pass 1
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 399.773655

✖ failing tests:

test at example.mjs:62:3
✖ throws myError from validation schema (11.012993ms)
AssertionError [ERR_ASSERTION]: Expected values to be strictly deep-equal:
+ actual - expected

+ {
- {
error: {
+ data: {
+ defaultError: Error: My message
+ at SuiteContext. (file:///[redacted]/example.mjs:10:24)
+ at Suite.runInAsyncScope (node:async_hooks:214:14)
+ at Suite.createBuild (node:internal/test_runner/test:1478:13)
+ at new Suite (node:internal/test_runner/test:1469:28)
+ at Test.createSubtest (node:internal/test_runner/test:865:18)
+ at run (node:internal/test_runner/harness:364:28)
+ at test (node:internal/test_runner/harness:378:12)
+ at file:///[redacted]/example.mjs:8:1
+ at ModuleJob.run (node:internal/modules/esm/module_job:413:25)
+ at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:660:26) {
+ data: [Circular *1],
+ isBoom: true,
+ isServer: false,
+ output: {
+ headers: {},
+ payload: [Circular *2],
+ statusCode: 400
+ }
+ }
- data: {
- my: 'data'
},
message: 'My message'
}
}

at TestContext. (file:///[redacted]/example.mjs:83:12)
at process.processTicksAndRejections (node:internal/process/task_queues:103:5)
at async Test.run (node:internal/test_runner/test:1113:7)
at async Suite.processPendingSubtests (node:internal/test_runner/test:788:7) {
generatedMessage: true,
code: 'ERR_ASSERTION',
actual: { error: { message: 'My message', data: [Object] } },
expected: { error: { message: 'My message', data: [Object] } },
operator: 'deepStrictEqual',
diff: 'simple'
}
```

### What result did you expect?

```console
$ node --test example.mjs
▶ issue showcase
✔ throws myError from handler (35.244367ms)
✔ throws myError from validation schema (12.917042ms)
✔ issue showcase (50.673704ms)
ℹ tests 2
ℹ suites 1
ℹ pass 2
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 718.821985
```

Contributor guide

Open the contributing guide

Research direction

Start with the example.mjs reproduction, especially the route's validate.query schema and the Joi error path, then compare it with the handler case and the onPreResponse formatter. Trace how the validator's error becomes the response and confirm that the expected data object is preserved; the reproduction's second assertion should pass.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript, node.js
Domain
api, backend
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
70/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.