Execution silently hangs when calling `readBody()` in `afterResponse` hook if prior handler did not invoke `readBody(event)`
Environment
node: >= 22.0.0 nitropack: 2.11.11
Reproduction
api/example.post.ts
export default defineEventHandler(() => {
// not calling `readBody(event)` here
return "test"
})
plugins/example.ts
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook("afterResponse", (event, response) => {
await readBody(event);
console.log("should run this")
// ^ this did not execute
});
});
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook("afterResponse", (event, response) => {
try {
await readBody(event);
} catch (error) {
console.error(error)
// ^ this did not execute
} finally {
console.log("should run this")
// ^ this did not execute
}
console.log("should run this")
// ^ this did not execute
});
});
Describe the bug
When invoking readBody(event) in afterResponse hook and if prior handler did not invoke readBody(event), the execution hangs silently with no error.
Additional context
A workaround fix would to ensure all readBody(event) is invoked regardless if handler is using or not:
nitroApp.hooks.hook("request", async (event) => {
if (
["post", "put", "patch", "delete"].includes(event.method.toLowerCase())
) {
await readBody(event); // ensures readBody(event) does not hang in `afterResponse` hook
}
});
Logs
I tried your reproduction steps, and everything works fine. Could you please confirm in this reproduction?
@kricsleo you're right – it's completely not reproducible with the provided examples on a fresh project (I've tried both on nitro and nuxt).
Perhaps there might be other factors at play. I'll dig deeper into the internals and update once I’ve identified the cause.
Meanwhile, in the real application, I also noticed that readBody() is being called within event.waitUntil. I'm not sure yet if that makes a difference, but for reference, here's the relevant snippet:
nitroApp.hooks.hook("afterResponse", (event, response) => {
event.waitUntil(afterResponseHandler(event, response))
});
async function afterResponseHandler(event, response) {
if (event.method.toLowerCase() === "get") return;
try {
console.log(await readBody(event));
} catch (error) {
console.error(error)
// ^ this did not execute
} finally {
console.log("should run finally")
// ^ this did not execute
}
console.log("should run at the end")
// ^ this did not execute
}
update:
After further digging, I found that the issue occurs inside the readRawBody utility from h3: https://github.com/h3js/h3/blob/401c9b8f149ace41d6404c4653472bcae8211594/src/utils/body.ts#L121
It seems that this internal Promise never resolves (or reject), causing the execution to hang silently.
Interestingly, both executions (from the example and the real application) are logically identical. There might be other factors at play that aren't directly visible. To also add on from my previous reply, event.waitUntil does not seem to be a factor, both with and without, execution still hangs.
If there are any parts of the codebase you'd recommend checking, or possible areas where readRawBody behavior might differ, I'd appreciate any pointers.
It's weird; I'm guessing the node.req never ends (e.g., a stream) in your real app?