Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions lib/dispatcher/client-h2.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand All @@ -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}`
}

Expand Down
73 changes: 73 additions & 0 deletions test/fetch/http2.js
Original file line number Diff line number Diff line change
@@ -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')
Expand Down Expand Up @@ -504,3 +505,75 @@ 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 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 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
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 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:${0}`)
})
Loading