From 7e7d096e401175b6cf7bdc5a1d9155fe119e2f1c Mon Sep 17 00:00:00 2001 From: Naor Peled Date: Sat, 8 Aug 2026 19:12:29 +0300 Subject: [PATCH] fix: don't write a response body for null body status codes Status codes that must not carry content per RFC 9110 (1xx, 204, 205, 304) were still emitting a body when set via status() and sent through send(), json(), html(), etc. Moves the check into send() so every response method inherits it, rather than special-casing sendStatus() alone. --- README.md | 12 +++++++++-- __tests__/responses.unit.js | 40 ++++++++++++++++++++++++++++++++++++ __tests__/utils.unit.js | 41 +++++++++++++++++++++---------------- src/lib/response.js | 7 ++++++- src/lib/utils.js | 15 ++++++-------- 5 files changed, 85 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 19302b7..c0c00b7 100644 --- a/README.md +++ b/README.md @@ -476,7 +476,15 @@ The `status` method allows you to set the status code that is returned to API Ga ```javascript api.get('/users', (req, res) => { - res.status(304).send('Not Modified'); + res.status(404).send('Not Found'); +}); +``` + +**NOTE:** Status codes that [must not carry content](https://datatracker.ietf.org/doc/html/rfc9110#name-overview-of-status-codes) per RFC 9110 — `1xx`, `204`, `205`, and `304` — are always sent with an empty body. Any body passed to `send()`, `json()`, `html()`, etc. is discarded for these status codes. + +```javascript +api.get('/users', (req, res) => { + res.status(204).json({ foo: 'bar' }); // sent as 204 with an empty body }); ``` @@ -489,7 +497,7 @@ res.sendStatus(200); // equivalent to res.status(200).send('OK') res.sendStatus(403); // equivalent to res.status(403).send('Forbidden') ``` -Status codes that [must not carry content](https://datatracker.ietf.org/doc/html/rfc9110#name-overview-of-status-codes) per RFC 9110 — `1xx`, `204`, `205`, and `304` — are sent with an empty body instead: +As with [`status()`](#statuscode), the `1xx`, `204`, `205`, and `304` status codes are sent with an empty body: ```javascript res.sendStatus(204); // equivalent to res.status(204).send('') diff --git a/__tests__/responses.unit.js b/__tests__/responses.unit.js index 6469d4d..3674c44 100644 --- a/__tests__/responses.unit.js +++ b/__tests__/responses.unit.js @@ -81,6 +81,22 @@ api.get('/testSendStatus403', function(req,res) { res.sendStatus(403) }) +api.get('/testNullBodyJson', function(req,res) { + res.status(204).json({ foo: 'bar' }) +}) + +api.get('/testNullBodySend', function(req,res) { + res.status(205).send('Reset Content') +}) + +api.get('/testNullBodyHtml', function(req,res) { + res.status(304).html('
Not Modified
') +}) + +api.get('/testNonNullBodyJson', function(req,res) { + res.status(200).json({ foo: 'bar' }) +}) + // Secondary route api2.get('/testJSONPResponse', function(req,res) { res.jsonp({ foo: 'bar' }) @@ -196,6 +212,30 @@ describe('Response Tests:', function() { expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 403, body: 'Forbidden', isBase64Encoded: false }) }) // end it + it('Null body status: json()', async function() { + let _event = Object.assign({},event,{ path: '/testNullBodyJson'}) + let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) + expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 204, body: '', isBase64Encoded: false }) + }) // end it + + it('Null body status: send()', async function() { + let _event = Object.assign({},event,{ path: '/testNullBodySend'}) + let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) + expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 205, body: '', isBase64Encoded: false }) + }) // end it + + it('Null body status: html()', async function() { + let _event = Object.assign({},event,{ path: '/testNullBodyHtml'}) + let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) + expect(result).toEqual({ multiValueHeaders: { 'content-type': ['text/html'] }, statusCode: 304, body: '', isBase64Encoded: false }) + }) // end it + + it('Non-null body status is unaffected', async function() { + let _event = Object.assign({},event,{ path: '/testNonNullBodyJson'}) + let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) + expect(result).toEqual({ multiValueHeaders: { 'content-type': ['application/json'] }, statusCode: 200, body: '{"foo":"bar"}', isBase64Encoded: false }) + }) // end it + it('JSONP response (default callback)', async function() { let _event = Object.assign({},event,{ path: '/testJSONPResponse' }) let result = await new Promise(r => api.run(_event,{},(e,res) => { r(res) })) diff --git a/__tests__/utils.unit.js b/__tests__/utils.unit.js index b6998ee..f692ccc 100644 --- a/__tests__/utils.unit.js +++ b/__tests__/utils.unit.js @@ -232,27 +232,32 @@ describe("Utility Function Tests:", function () { }); // end it }); // end encodeBody tests - describe("statusBodyLookup:", function () { + describe("isNullBodyStatus:", function () { test.each([ - [100, ""], - [101, ""], - [200, "OK"], - ["200", "OK"], - [204, ""], - ["204", ""], - [205, ""], - ["205", ""], - [304, ""], - ["304", ""], - [404, "Not Found"], - [502, "Bad Gateway"], - [999, "Unknown"], - ["not a number", "Unknown"] + [100, true], + [101, true], + [199, true], + [204, true], + [205, true], + [304, true], + ["204", true], + ["304", true], + [200, false], + [201, false], + [203, false], + [206, false], + [303, false], + [305, false], + [404, false], + [502, false], + ["200", false], + ["foo", false], + [undefined, false], + [null, false], ])("%s", (status, expected) => { - expect(utils.statusBodyLookup(status)).toBe(expected); + expect(utils.isNullBodyStatus(status)).toBe(expected); }); // end it - }); // end statusBodyLookup tests - + }); // end isNullBodyStatus tests describe("extractRoutes:", function () { it("Sample routes", function () { diff --git a/src/lib/response.js b/src/lib/response.js index b59ce38..81b1d0d 100644 --- a/src/lib/response.js +++ b/src/lib/response.js @@ -415,7 +415,7 @@ class RESPONSE { // Convenience method for sending status codes sendStatus(status) { - this.status(status).send(UTILS.statusBodyLookup(status)); + this.status(status).send(UTILS.statusLookup(status)); } // Convenience method for setting CORS headers @@ -525,6 +525,11 @@ class RESPONSE { body = ''; } + // Discard the body for status codes that must not include content + if (UTILS.isNullBodyStatus(this._statusCode)) { + body = ''; + } + let headers = {}; let cookies = {}; diff --git a/src/lib/utils.js b/src/lib/utils.js index 22a02dc..8c294d1 100644 --- a/src/lib/utils.js +++ b/src/lib/utils.js @@ -114,16 +114,13 @@ export const statusLookup = (status) => { return status in statusCodes ? statusCodes[status] : 'Unknown'; }; -export const statusBodyLookup = (status) => { +// Status codes that must not include content per RFC 9110 +// https://datatracker.ietf.org/doc/html/rfc9110#name-overview-of-status-codes +export const isNullBodyStatus = (status) => { const code = typeof status === 'string' ? Number(status) : status; - - // The following status codes must not have a response body - // according to rfc 9110 - if ((100 <= code && code < 200) || [204, 205, 304].includes(code)) { - return ''; - } - - return statusLookup(code); + return ( + (code >= 100 && code < 200) || code === 204 || code === 205 || code === 304 + ); }; // Parses routes into readable array