Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion lib/api/api-connect.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ class ConnectHandler extends AsyncResource {
// Indicates is an HTTP2Session
if (responseHeaders != null) {
responseHeaders = this.responseHeaders === 'raw'
? (Array.isArray(rawHeaders) ? util.parseRawHeaders(rawHeaders) : [])
? util.parseRawHeaders(rawHeaders)
: headers
}

Expand Down
4 changes: 2 additions & 2 deletions lib/api/api-pipeline.js
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ class PipelineHandler extends AsyncResource {
if (this.onInfo) {
const rawHeaders = controller?.rawHeaders
const responseHeaders = this.responseHeaders === 'raw'
? (Array.isArray(rawHeaders) ? util.parseRawHeaders(rawHeaders) : [])
? util.parseRawHeaders(rawHeaders)
: headers
this.onInfo({ statusCode, headers: responseHeaders })
}
Expand All @@ -181,7 +181,7 @@ class PipelineHandler extends AsyncResource {
this.handler = null
const rawHeaders = controller?.rawHeaders
const responseHeaders = this.responseHeaders === 'raw'
? (Array.isArray(rawHeaders) ? util.parseRawHeaders(rawHeaders) : [])
? util.parseRawHeaders(rawHeaders)
: headers
body = this.runInAsyncScope(handler, null, {
statusCode,
Expand Down
2 changes: 1 addition & 1 deletion lib/api/api-request.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ class RequestHandler extends AsyncResource {

const rawHeaders = controller?.rawHeaders
const responseHeaderData = responseHeaders === 'raw'
? (Array.isArray(rawHeaders) ? util.parseRawHeaders(rawHeaders) : [])
? util.parseRawHeaders(rawHeaders)
: headers

if (statusCode < 200) {
Expand Down
2 changes: 1 addition & 1 deletion lib/api/api-stream.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ class StreamHandler extends AsyncResource {

const rawHeaders = controller?.rawHeaders
const responseHeaderData = responseHeaders === 'raw'
? (Array.isArray(rawHeaders) ? util.parseRawHeaders(rawHeaders) : [])
? util.parseRawHeaders(rawHeaders)
: headers

if (statusCode < 200) {
Expand Down
2 changes: 1 addition & 1 deletion lib/api/api-upgrade.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ class UpgradeHandler extends AsyncResource {

const rawHeaders = controller?.rawHeaders
const responseHeaders = this.responseHeaders === 'raw'
? (Array.isArray(rawHeaders) ? util.parseRawHeaders(rawHeaders) : [])
? util.parseRawHeaders(rawHeaders)
: headers

this.runInAsyncScope(callback, null, null, {
Expand Down
22 changes: 21 additions & 1 deletion lib/core/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -464,10 +464,30 @@ function parseHeaders (headers, obj) {
}

/**
* @param {Buffer[]} headers
* @param {Buffer[] | string[] | Record<string, string | string[]> | null | undefined} headers
* @returns {string[]}
*/
function parseRawHeaders (headers) {
if (headers == null) {
return []
}

if (!Array.isArray(headers)) {
const rawHeaders = []

for (const [name, value] of Object.entries(headers)) {
if (Array.isArray(value)) {
for (const entry of value) {
rawHeaders.push(name, `${entry}`)
}
} else {
rawHeaders.push(name, `${value}`)
}
}

return rawHeaders
}

const headersLength = headers.length
/**
* @type {string[]}
Expand Down
24 changes: 2 additions & 22 deletions lib/dispatcher/client-h2.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,26 +72,6 @@ const {
}
} = http2

function parseH2Headers (headers) {
const result = []

for (const [name, value] of Object.entries(headers)) {
// h2 may concat the header value by array
// e.g. Set-Cookie
if (Array.isArray(value)) {
for (const subvalue of value) {
// we need to provide each header value of header name
// because the headers handler expect name-value pair
result.push(Buffer.from(name), Buffer.from(subvalue))
}
} else {
result.push(Buffer.from(name), Buffer.from(value))
}
}

return result
}

function getGoAwayError (session, errorCode) {
return session[kError] ||
(errorCode === NGHTTP2_NO_ERROR
Expand Down Expand Up @@ -695,7 +675,7 @@ function writeH2 (client, request) {

const { [HTTP2_HEADER_STATUS]: statusCode, ...realHeaders } = headers

request.onRequestUpgrade(statusCode, parseH2Headers(realHeaders), stream)
request.onRequestUpgrade(statusCode, realHeaders, stream)

if (request.aborted || request.completed) {
return
Expand Down Expand Up @@ -928,7 +908,7 @@ function writeH2 (client, request) {
return
}

if (request.onResponseStart(Number(statusCode), parseH2Headers(realHeaders), stream.resume.bind(stream), '') === false) {
if (request.onResponseStart(Number(statusCode), realHeaders, stream.resume.bind(stream), '') === false) {
stream.pause()
}

Expand Down
59 changes: 33 additions & 26 deletions lib/web/fetch/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,35 @@ const defaultUserAgent = typeof __UNDICI_IS_NODE__ !== 'undefined' || typeof esb
/** @type {import('buffer').resolveObjectURL} */
let resolveObjectURL

function appendHeadersListFromResponseHeaders (headersList, headers, rawHeaders) {
if (Array.isArray(rawHeaders)) {
for (let i = 0; i < rawHeaders.length; i += 2) {
const nameStr = bufferToLowerCasedHeaderName(rawHeaders[i])
const value = rawHeaders[i + 1]

if (Array.isArray(value) && !Buffer.isBuffer(value)) {
for (const val of value) {
headersList.append(nameStr, val.toString('latin1'), true)
}
} else {
headersList.append(nameStr, value.toString('latin1'), true)
}
}

return
}

for (const [name, value] of Object.entries(headers ?? {})) {
if (Array.isArray(value)) {
for (const entry of value) {
headersList.append(name, `${entry}`, true)
}
} else {
headersList.append(name, `${value}`, true)
}
}
}

class Fetch extends EE {
constructor (dispatcher) {
super()
Expand Down Expand Up @@ -2196,25 +2225,14 @@ async function httpNetworkFetch (
timingInfo.finalNetworkResponseStartTime = coarsenedSharedCurrentTime(fetchParams.crossOriginIsolatedCapability)
},

onResponseStart (controller, status, _headers, statusText) {
onResponseStart (controller, status, headers, statusText) {
if (status < 200) {
return
}

const rawHeaders = controller?.rawHeaders ?? []
const headersList = new HeadersList()

for (let i = 0; i < rawHeaders.length; i += 2) {
const nameStr = bufferToLowerCasedHeaderName(rawHeaders[i])
const value = rawHeaders[i + 1]
if (Array.isArray(value) && !Buffer.isBuffer(rawHeaders[i + 1])) {
for (const val of value) {
headersList.append(nameStr, val.toString('latin1'), true)
}
} else {
headersList.append(nameStr, value.toString('latin1'), true)
}
}
appendHeadersListFromResponseHeaders(headersList, headers, rawHeaders)
const location = headersList.get('location', true)

this.body = new Readable({ read: () => controller.resume() })
Expand Down Expand Up @@ -2349,7 +2367,7 @@ async function httpNetworkFetch (
reject(error)
},

onRequestUpgrade (controller, status, _headers, socket) {
onRequestUpgrade (controller, status, headers, socket) {
// We need to support 200 for websocket over h2 as per RFC-8441
// Absence of session means H1
if ((socket.session != null && status !== 200) || (socket.session == null && status !== 101)) {
Expand All @@ -2358,18 +2376,7 @@ async function httpNetworkFetch (

const rawHeaders = controller?.rawHeaders ?? []
const headersList = new HeadersList()

for (let i = 0; i < rawHeaders.length; i += 2) {
const nameStr = bufferToLowerCasedHeaderName(rawHeaders[i])
const value = rawHeaders[i + 1]
if (Array.isArray(value) && !Buffer.isBuffer(rawHeaders[i + 1])) {
for (const val of value) {
headersList.append(nameStr, val.toString('latin1'), true)
}
} else {
headersList.append(nameStr, value.toString('latin1'), true)
}
}
appendHeadersListFromResponseHeaders(headersList, headers, rawHeaders)

resolve({
status,
Expand Down
61 changes: 53 additions & 8 deletions test/http2-body.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ test('Should handle h2 request without body', async t => {
})

test('Should handle h2 request with body (string or buffer) - dispatch', async t => {
t = tspl(t, { plan: 9 })
t = tspl(t, { plan: 7 })

const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } }))
const expectedBody = 'hello from client!'
Expand Down Expand Up @@ -121,13 +121,8 @@ test('Should handle h2 request with body (string or buffer) - dispatch', async t
onResponseStart (controller, statusCode) {
const rawHeaders = controller.rawHeaders
t.strictEqual(statusCode, 200)
t.strictEqual(rawHeaders[0].toString('utf-8'), 'content-type')
t.strictEqual(
rawHeaders[1].toString('utf-8'),
'text/plain; charset=utf-8'
)
t.strictEqual(rawHeaders[2].toString('utf-8'), 'x-custom-h2')
t.strictEqual(rawHeaders[3].toString('utf-8'), 'foo')
t.strictEqual(rawHeaders['content-type'], 'text/plain; charset=utf-8')
t.strictEqual(rawHeaders['x-custom-h2'], 'foo')
},
onResponseData (_controller, chunk) {
response.push(chunk)
Expand All @@ -148,6 +143,56 @@ test('Should handle h2 request with body (string or buffer) - dispatch', async t
await t.completed
})

test('Should handle h2 request raw response headers', async t => {
t = tspl(t, { plan: 4 })

const server = createSecureServer(await pem.generate({ opts: { keySize: 2048 } }))

server.on('stream', (stream, headers) => {
stream.respond({
'content-type': 'text/plain; charset=utf-8',
'x-custom-h2': headers['x-my-header'],
':status': 200
})

stream.end('hello h2!')
})

after(() => server.close())
await once(server.listen(0), 'listening')

const client = new Client(`https://localhost:${server.address().port}`, {
connect: {
rejectUnauthorized: false
},
allowH2: true
})
after(() => client.close())

const { statusCode, headers, body } = await client.request({
path: '/',
method: 'GET',
headers: {
'x-my-header': 'foo'
},
responseHeaders: 'raw'
})

await body.dump()

const rawHeaders = Object.create(null)
for (let i = 0; i < headers.length; i += 2) {
rawHeaders[headers[i]] = headers[i + 1]
}

t.strictEqual(statusCode, 200)
t.strictEqual(Array.isArray(headers), true)
t.strictEqual(rawHeaders['content-type'], 'text/plain; charset=utf-8')
t.strictEqual(rawHeaders['x-custom-h2'], 'foo')

await t.completed
})

test('Should handle h2 request with body (stream)', async t => {
t = tspl(t, { plan: 8 })

Expand Down
3 changes: 3 additions & 0 deletions test/node-test/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -159,8 +159,11 @@ test('parseHeaders decodes array header values as latin1', () => {
})

test('parseRawHeaders', () => {
assert.deepEqual(util.parseRawHeaders(), [])
assert.deepEqual(util.parseRawHeaders(null), [])
assert.deepEqual(util.parseRawHeaders(['key', 'value', Buffer.from('key'), Buffer.from('value')]), ['key', 'value', 'key', 'value'])
assert.deepEqual(util.parseRawHeaders(['content-length', 'value', 'content-disposition', 'form-data; name="fieldName"']), ['content-length', 'value', 'content-disposition', 'form-data; name="fieldName"'])
assert.deepEqual(util.parseRawHeaders({ key: 'value', 'set-cookie': ['a=1', 'b=2'] }), ['key', 'value', 'set-cookie', 'a=1', 'set-cookie', 'b=2'])
})

test('parseRawHeaders decodes values as latin1, not utf8', () => {
Expand Down
4 changes: 2 additions & 2 deletions types/dispatcher.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,8 +210,8 @@ declare namespace Dispatcher {
get aborted(): boolean
get paused(): boolean
get reason(): Error | null
rawHeaders?: Buffer[] | string[] | null
rawTrailers?: Buffer[] | string[] | null
rawHeaders?: Buffer[] | string[] | IncomingHttpHeaders | null
rawTrailers?: Buffer[] | string[] | IncomingHttpHeaders | null
abort(reason: Error): void
pause(): void
resume(): void
Expand Down
Loading