205 and 304 responses still send a message body
Prerequisites
- I have written a descriptive issue title
- I have searched existing issues to ensure the bug has not been reported
Fastify version
6.0.0-alpha.3
Plugin version
No response
Node.js version
26.8.1
Operating system
macOS
Operating system version
(latest)
Description
Replying with 205 or 304 and a body still puts that body on the wire.
Neither code is allowed to carry one:
- RFC 9110 §15.3.6 — "a server MUST NOT generate content in a 205 response"
- RFC 9110 §15.4.5 — "A 304 response cannot contain a message body"
The cause is in lib/reply.js. onSendEnd() contains two "bodyless status" checks, and the two lists don't agree with each other:
// line 651 — this one already knows 304 is bodyless
if (statusCode >= 200 && statusCode !== 204 && statusCode !== 304 && req.method !== 'HEAD' && ...) {
// line 660 — this one only knows 1xx and 204
if ((statusCode >= 100 && statusCode < 200) || statusCode === 204) {A 205 or 304 that carries a payload only ever reaches line 660, so nothing strips the body and it gets written to the socket.
204 is handled correctly, which is what makes 205 and 304 look like two cases that were simply missed.
Steps to Reproduce
The script below talks to a real server over a raw socket, so the bytes printed are the bytes actually sent:
'use strict'
const Fastify = require('fastify')
const net = require('node:net')
const BODY = 'BODY-CONTENT'
async function main () {
const app = Fastify({ logger: false })
app.get('/204', async (req, reply) => { reply.code(204).send(BODY) })
app.get('/205', async (req, reply) => { reply.code(205).send(BODY) })
app.get('/304', async (req, reply) => { reply.code(304).send(BODY) })
await app.listen({ port: 0, host: '127.0.0.1' })
const { port } = app.server.address()
for (const path of ['/204', '/205', '/304']) {
const raw = await new Promise((resolve, reject) => {
const socket = net.connect(port, '127.0.0.1', () => {
socket.write(`GET ${path} HTTP/1.1\r\nHost: 127.0.0.1:${port}\r\nConnection: close\r\n\r\n`)
})
const chunks = []
socket.on('data', (c) => chunks.push(c))
socket.on('end', () => resolve(Buffer.concat(chunks).toString('latin1')))
socket.on('error', reject)
})
const [head, ...rest] = raw.split('\r\n\r\n')
const body = rest.join('\r\n\r\n')
console.log(`\n── GET ${path} ──`)
console.log(head)
console.log(`body bytes = ${Buffer.byteLength(body)} body = ${JSON.stringify(body)}`)
}
await app.close()
}
main()Actual Behaviour

── GET /204 ── content-length: (absent) body bytes = 0
── GET /205 ── content-length: 12 body bytes = 12 "BODY-CONTENT"
── GET /304 ── content-length: 12 body bytes = 0204 sends nothing at all (correct). 205 really writes those 12 bytes. 304 writes no body but still advertises content-length: 12, so the header disagrees with what is sent.
304 is also inconsistent between transports: inject() returns the full body, while a real socket returns nothing.
Expected Behaviour
205 and 304 should behave exactly like 204: no body, and no content-length that contradicts what is actually sent.
Debugger evidence
A conditional breakpoint on line 660 (statusCode === 205) shows the reason directly — statusCode is 205, but the check evaluates to false, so execution walks straight past the branch that would have dropped the payload:

And here are the two checks side by side. Line 651 knows about 304; line 660 does not know about 205 or 304:

Link to code that reproduces the bug
The script under Steps to Reproduce is the complete reproduction — no dependencies beyond Fastify itself.
Source: fastify/fastify