From ff97f0c8e7d807cf8a095d760d1c84b0d46d2375 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Delaporte?= <12201973+fredericdelaporte@users.noreply.github.com> Date: Sun, 5 Oct 2025 17:42:27 +0200 Subject: [PATCH 1/3] Add tests for empty POST and content-length --- test/fetch/http2.js | 75 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/test/fetch/http2.js b/test/fetch/http2.js index 75f0ec08bab..151034b5930 100644 --- a/test/fetch/http2.js +++ b/test/fetch/http2.js @@ -1,6 +1,7 @@ 'use strict' const { createSecureServer } = require('node:http2') +const { createServer } = require('node:http') const { createReadStream, readFileSync } = require('node:fs') const { once } = require('node:events') const { Readable } = require('node:stream') @@ -504,3 +505,77 @@ test('Issue #3046', async (t) => { t.assert.strictEqual(response.headers.get('content-type'), 'text/html; charset=utf-8') t.assert.deepStrictEqual(response.headers.getSetCookie(), ['hello=world', 'foo=bar']) }) + +// The two following tests expose a discrepancy of behavior when enabling HTTP/2. +// Without H2 enabled, empty POST requests have a Content-Length of 0 specified. +// With H2 enabled, empty POST requests do not have a Content-Length header. +// The RFC 9110 (see https://httpwg.org/specs/rfc9110.html#field.content-length) +// states it SHOULD have one, so that is not mandatory, but is there a good reason +// for not having it? +test('[Fetch] Empty POST without h2 has Content-Length', async (t) => { + const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { + res.statusCode = 200 + res.end(`content-length:${req.headers['content-length']}`) + }).listen(0) + + const client = new Client(`http://localhost:${server.address().port}`) + + t.after(async () => { + server.close() + await client.close() + }) + + t.plan(1) + + await once(server, 'listening') + + const response = await fetch( + `http://localhost:${server.address().port}/`, { + method: 'POST', + dispatcher: client + } + ) + + const responseBody = await response.text() + t.assert.strictEqual(responseBody, `content-length:${0}`) +}) + +test('[Fetch] Empty POST with h2 has no Content-Length', async (t) => { + const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } })) + + server.on('stream', async (stream, headers) => { + stream.respond({ + 'content-type': 'text/plain; charset=utf-8', + ':status': 200 + }) + + stream.end(`content-length:${headers['content-length']}`) + }) + + t.plan(1) + + server.listen() + await once(server, 'listening') + + const client = new Client(`https://localhost:${server.address().port}`, { + connect: { + rejectUnauthorized: false + }, + allowH2: true + }) + + t.after(closeClientAndServerAsPromise(client, server)) + + const response = await fetch( + `https://localhost:${server.address().port}/`, + // Needs to be passed to disable the reject unauthorized + { + method: 'POST', + dispatcher: client + } + ) + + const responseBody = await response.text() + + t.assert.strictEqual(responseBody, `content-length:${undefined}`) +}) From 5d896aada408165802b3d6fed274feeb4ddd0a66 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Sun, 5 Oct 2025 21:00:21 +0200 Subject: [PATCH 2/3] fix: ensure HTTP/2 sends Content-Length for empty POST requests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This change aligns HTTP/2 behavior with HTTP/1 by ensuring that empty POST requests (and other methods that expect payloads) include a Content-Length: 0 header. According to RFC 9110, a user agent SHOULD send Content-Length in a request when the method defines a meaning for enclosed content, even when the value is 0 (indicating empty content). Previously, HTTP/2 requests would omit the Content-Length header for empty POST requests, while HTTP/1 requests correctly included it. This inconsistency could cause issues with servers like IIS that reject empty POST requests without a Content-Length header (returning 411 Length Required). Changes: - Modified lib/dispatcher/client-h2.js to only omit Content-Length when both contentLength is 0 AND the method doesn't expect a payload - Updated the assertion to allow contentLength === 0 without a body - Updated test to verify Content-Length: 0 is sent for empty POST with H2 - Updated test comments to reflect the corrected behavior Fixes #4594 Ref #4612 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Signed-off-by: Matteo Collina --- lib/dispatcher/client-h2.js | 7 +++++-- test/fetch/http2.js | 12 +++++------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/lib/dispatcher/client-h2.js b/lib/dispatcher/client-h2.js index 5776f4fb207..3bfc521a261 100644 --- a/lib/dispatcher/client-h2.js +++ b/lib/dispatcher/client-h2.js @@ -429,12 +429,15 @@ function writeH2 (client, request) { contentLength = request.contentLength } - if (contentLength === 0 || !expectsPayload) { + if (contentLength === 0 && !expectsPayload) { // https://tools.ietf.org/html/rfc7230#section-3.3.2 // A user agent SHOULD NOT send a Content-Length header field when // the request message does not contain a payload body and the method // semantics do not anticipate such a body. + contentLength = null + } else if (!expectsPayload) { + // For methods that don't expect a payload, omit Content-Length contentLength = null } @@ -450,7 +453,7 @@ function writeH2 (client, request) { } if (contentLength != null) { - assert(body, 'no body must not have content length') + assert(body || contentLength === 0, 'no body must not have content length') headers[HTTP2_HEADER_CONTENT_LENGTH] = `${contentLength}` } diff --git a/test/fetch/http2.js b/test/fetch/http2.js index 151034b5930..6ee14aabd7a 100644 --- a/test/fetch/http2.js +++ b/test/fetch/http2.js @@ -506,12 +506,10 @@ test('Issue #3046', async (t) => { t.assert.deepStrictEqual(response.headers.getSetCookie(), ['hello=world', 'foo=bar']) }) -// The two following tests expose a discrepancy of behavior when enabling HTTP/2. -// Without H2 enabled, empty POST requests have a Content-Length of 0 specified. -// With H2 enabled, empty POST requests do not have a Content-Length header. +// The two following tests ensure that empty POST requests have a Content-Length of 0 +// specified, both with and without HTTP/2 enabled. // The RFC 9110 (see https://httpwg.org/specs/rfc9110.html#field.content-length) -// states it SHOULD have one, so that is not mandatory, but is there a good reason -// for not having it? +// states it SHOULD have one for methods like POST that define a meaning for enclosed content. test('[Fetch] Empty POST without h2 has Content-Length', async (t) => { const server = createServer({ joinDuplicateHeaders: true }, (req, res) => { res.statusCode = 200 @@ -540,7 +538,7 @@ test('[Fetch] Empty POST without h2 has Content-Length', async (t) => { t.assert.strictEqual(responseBody, `content-length:${0}`) }) -test('[Fetch] Empty POST with h2 has no Content-Length', async (t) => { +test('[Fetch] Empty POST with h2 has Content-Length', async (t) => { const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } })) server.on('stream', async (stream, headers) => { @@ -577,5 +575,5 @@ test('[Fetch] Empty POST with h2 has no Content-Length', async (t) => { const responseBody = await response.text() - t.assert.strictEqual(responseBody, `content-length:${undefined}`) + t.assert.strictEqual(responseBody, `content-length:${0}`) }) From e83e4e3c160e649893aeb07c2e200d829be3e189 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Sun, 12 Oct 2025 09:34:34 +0200 Subject: [PATCH 3/3] Update client-h2.js MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Frédéric Delaporte <12201973+fredericDelaporte@users.noreply.github.com> --- lib/dispatcher/client-h2.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/lib/dispatcher/client-h2.js b/lib/dispatcher/client-h2.js index 3bfc521a261..090ca7ab582 100644 --- a/lib/dispatcher/client-h2.js +++ b/lib/dispatcher/client-h2.js @@ -429,15 +429,12 @@ function writeH2 (client, request) { contentLength = request.contentLength } - if (contentLength === 0 && !expectsPayload) { + if (!expectsPayload) { // https://tools.ietf.org/html/rfc7230#section-3.3.2 // A user agent SHOULD NOT send a Content-Length header field when // the request message does not contain a payload body and the method // semantics do not anticipate such a body. - - contentLength = null - } else if (!expectsPayload) { - // For methods that don't expect a payload, omit Content-Length + // And for methods that don't expect a payload, omit Content-Length. contentLength = null }