Skip to content

Commit 3df1696

Browse files
KhafraDevmetcoder95
authored andcommitted
fix: sending formdata bodies with http2 (#3863)
(cherry picked from commit e49b575)
1 parent f21da44 commit 3df1696

File tree

2 files changed

+141
-2
lines changed

2 files changed

+141
-2
lines changed

lib/dispatcher/client-h2.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ const {
3131

3232
const kOpenStreams = Symbol('open streams')
3333

34+
let extractBody
35+
3436
// Experimental
3537
let h2ExperimentalWarned = false
3638

@@ -260,7 +262,8 @@ function shouldSendContentLength (method) {
260262

261263
function writeH2 (client, request) {
262264
const session = client[kHTTP2Session]
263-
const { body, method, path, host, upgrade, expectContinue, signal, headers: reqHeaders } = request
265+
const { method, path, host, upgrade, expectContinue, signal, headers: reqHeaders } = request
266+
let { body } = request
264267

265268
if (upgrade) {
266269
util.errorRequest(client, request, new Error('Upgrade not supported for H2'))
@@ -381,6 +384,16 @@ function writeH2 (client, request) {
381384

382385
let contentLength = util.bodyLength(body)
383386

387+
if (util.isFormDataLike(body)) {
388+
extractBody ??= require('../web/fetch/body.js').extractBody
389+
390+
const [bodyStream, contentType] = extractBody(body)
391+
headers['content-type'] = contentType
392+
393+
body = bodyStream.stream
394+
contentLength = bodyStream.length
395+
}
396+
384397
if (contentLength == null) {
385398
contentLength = request.contentLength
386399
}

test/http2.js

Lines changed: 127 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const { Writable, pipeline, PassThrough, Readable } = require('node:stream')
1010

1111
const pem = require('https-pem')
1212

13-
const { Client, Agent } = require('..')
13+
const { Client, Agent, FormData } = require('..')
1414

1515
const isGreaterThanv20 = process.versions.node.split('.').map(Number)[0] >= 20
1616

@@ -1450,3 +1450,129 @@ test('#3671 - Graceful close', async (t) => {
14501450

14511451
await t.completed
14521452
})
1453+
1454+
test('#3753 - Handle GOAWAY Gracefully', async (t) => {
1455+
const server = createSecureServer(pem)
1456+
let counter = 0
1457+
let session = null
1458+
1459+
server.on('session', s => {
1460+
session = s
1461+
})
1462+
1463+
server.on('stream', (stream) => {
1464+
counter++
1465+
1466+
// Due to the nature of the test, we need to ignore the error
1467+
// that is thrown when the session is destroyed and stream
1468+
// is in-flight
1469+
stream.on('error', () => {})
1470+
if (counter === 9 && session != null) {
1471+
session.goaway()
1472+
stream.end()
1473+
} else {
1474+
stream.respond({
1475+
'content-type': 'text/plain',
1476+
':status': 200
1477+
})
1478+
setTimeout(() => {
1479+
stream.end('hello world')
1480+
}, 150)
1481+
}
1482+
})
1483+
1484+
server.listen(0)
1485+
await once(server, 'listening')
1486+
1487+
const client = new Client(`https://localhost:${server.address().port}`, {
1488+
connect: {
1489+
rejectUnauthorized: false
1490+
},
1491+
pipelining: 2,
1492+
allowH2: true
1493+
})
1494+
1495+
t = tspl(t, { plan: 30 })
1496+
after(() => client.close())
1497+
after(() => server.close())
1498+
1499+
for (let i = 0; i < 15; i++) {
1500+
client.request({
1501+
path: '/',
1502+
method: 'GET',
1503+
headers: {
1504+
'x-my-header': 'foo'
1505+
}
1506+
}, (err, response) => {
1507+
if (err) {
1508+
t.strictEqual(err.message, 'HTTP/2: "GOAWAY" frame received with code 0')
1509+
t.strictEqual(err.code, 'UND_ERR_SOCKET')
1510+
} else {
1511+
t.strictEqual(response.statusCode, 200)
1512+
;(async function () {
1513+
let body
1514+
try {
1515+
body = await response.body.text()
1516+
} catch (err) {
1517+
t.strictEqual(err.code, 'UND_ERR_SOCKET')
1518+
return
1519+
}
1520+
t.strictEqual(body, 'hello world')
1521+
})()
1522+
}
1523+
})
1524+
}
1525+
1526+
await t.completed
1527+
})
1528+
1529+
test('#3803 - sending FormData bodies works', async (t) => {
1530+
const assert = tspl(t, { plan: 4 })
1531+
1532+
const server = createSecureServer(pem).listen(0)
1533+
server.on('stream', async (stream, headers) => {
1534+
const contentLength = Number(headers['content-length'])
1535+
1536+
assert.ok(!Number.isNaN(contentLength))
1537+
assert.ok(headers['content-type']?.startsWith('multipart/form-data; boundary='))
1538+
1539+
stream.respond({ ':status': 200 })
1540+
1541+
const fd = await new Response(stream, {
1542+
headers: {
1543+
'content-type': headers['content-type']
1544+
}
1545+
}).formData()
1546+
1547+
assert.deepEqual(fd.get('a'), 'b')
1548+
assert.deepEqual(fd.get('c').name, 'e.fgh')
1549+
1550+
stream.end()
1551+
})
1552+
1553+
await once(server, 'listening')
1554+
1555+
const client = new Client(`https://localhost:${server.address().port}`, {
1556+
connect: {
1557+
rejectUnauthorized: false
1558+
},
1559+
allowH2: true
1560+
})
1561+
1562+
t.after(async () => {
1563+
server.close()
1564+
await client.close()
1565+
})
1566+
1567+
const fd = new FormData()
1568+
fd.set('a', 'b')
1569+
fd.set('c', new Blob(['d']), 'e.fgh')
1570+
1571+
await client.request({
1572+
path: '/',
1573+
method: 'POST',
1574+
body: fd
1575+
})
1576+
1577+
await assert.completed
1578+
})

0 commit comments

Comments
 (0)