Skip to content

fix: send no body for HEAD requests and 204/304 responses - #408

Open
lprnmns wants to merge 1 commit into
fastify:mainfrom
lprnmns:fix/no-body-for-head
Open

lprnmns wants to merge 1 commit into
fastify:mainfrom
lprnmns:fix/no-body-for-head

Conversation

@lprnmns

@lprnmns lprnmns commented Oct 23, 2026

Copy link
Copy Markdown

inject() returns the body a handler writes for a HEAD request, or for a 204 or 304 response. A real Node.js server never sends it: http.ServerResponse sets _hasBody = false in these cases and ignores the writes.

const dispatch = (req, res) => {
  res.writeHead(200, { 'content-type': 'text/plain', 'content-length': 5 })
  res.end('hello')
}

await inject(dispatch, { method: 'HEAD', url: '/' })   // payload: 'hello'
// the same handler behind http.createServer() + a HEAD request: no body

I found this by sending the same requests to a dispatch function through inject() and through a real http.createServer(). It is the same kind of mismatch that #388 fixed for request headers.

The fix checks this._hasBody in Response.prototype.write after delegating to http.ServerResponse, and drops the chunk when Node would (both the buffered payload and payloadAsStream). Headers, including content-length, are kept as they are, as on a real server.

Tests: three new cases at the end of test/index.test.js (HEAD, 204/304, HEAD with payloadAsStream) fail without the change. "chainable api: http methods should work correctly" echoed req.method in the body and expected it back for head too. It now expects an empty body for head. npm test (182 tests, 100% coverage, tstyche) and npm run lint pass.

Note for fastify: I ran fastify's test suite against this branch (by symlinking it into node_modules). 2 of 2388 tests read a HEAD body through fastify.inject(): "Should allow registering a constrained GET route after a constrained HEAD route" (test/constrained-routes.test.js) and "fastify.all should add all the methods to the same url" (test/internals/all.test.js, which parses the HEAD payload as JSON). A real fastify server sends no body in either case. I'm happy to open the matching fastify PR if you want to take this.

@gurgunday gurgunday left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The behaviour is correct, but with this branch in place 2 fastify core tests fail that pass without it. They are constrained-routes.test.js:640 ("constrained GET route after a constrained HEAD route") and "fastify.all should add all the methods" in test/internals/all.test.js, both asserting a HEAD body through inject(). Fastify core needs updating before this ships in a minor release.

@lprnmns

lprnmns commented Oct 24, 2026

Copy link
Copy Markdown
Author

Thanks! I opened fastify/fastify#7148, which updates those two tests to check the HEAD response headers (content-type / content-length) instead of the body. They pass with both the current light-my-request release and this branch, so it can land first.

gurgunday pushed a commit to fastify/fastify that referenced this pull request Oct 24, 2026
Two tests read the body of a HEAD response through `fastify.inject()`:

- "Should allow registering a constrained GET route after a constrained
HEAD route" (`test/constrained-routes.test.js`) checks `res.payload ===
'custom HEAD response'`;
- "fastify.all should add all the methods to the same url"
(`test/internals/all.test.js`) parses the HEAD payload as JSON.

A real server sends no body for HEAD, and fastify/light-my-request#408
makes `inject()` do the same. @gurgunday approved the behaviour there
but asked for fastify core to be updated before it ships, since these
two tests would fail with it.

These tests now check the response headers instead of the body:

- The constrained route test asserts the `content-type` set by the
custom HEAD handler (`text/plain`) and the `content-length` of `'custom
HEAD response'`. That still shows the custom handler ran, not a HEAD
route derived from the GET one.
- The `fastify.all` test asserts the HEAD response's `content-length`
matches `JSON.stringify({ method: 'HEAD' })` and parses the body only
for the other methods.

Both files pass with the current light-my-request release and with the
#408 branch symlinked into `node_modules` (26 tests each time), so this
can land before or after it. ESLint is clean on both files.

@gurgunday gurgunday left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

Tony133 pushed a commit to fastify/fastify that referenced this pull request Oct 24, 2026
Two tests read the body of a HEAD response through `fastify.inject()`:

- "Should allow registering a constrained GET route after a constrained
HEAD route" (`test/constrained-routes.test.js`) checks `res.payload ===
'custom HEAD response'`;
- "fastify.all should add all the methods to the same url"
(`test/internals/all.test.js`) parses the HEAD payload as JSON.

A real server sends no body for HEAD, and fastify/light-my-request#408
makes `inject()` do the same. @gurgunday approved the behaviour there
but asked for fastify core to be updated before it ships, since these
two tests would fail with it.

These tests now check the response headers instead of the body:

- The constrained route test asserts the `content-type` set by the
custom HEAD handler (`text/plain`) and the `content-length` of `'custom
HEAD response'`. That still shows the custom handler ran, not a HEAD
route derived from the GET one.
- The `fastify.all` test asserts the HEAD response's `content-length`
matches `JSON.stringify({ method: 'HEAD' })` and parses the body only
for the other methods.

Both files pass with the current light-my-request release and with the
#408 branch symlinked into `node_modules` (26 tests each time), so this
can land before or after it. ESLint is clean on both files.

(cherry picked from commit 78d68b2)

This branch has not been deployed

No deployments
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

Successfully merging this pull request may close these issues.

2 participants