From 158ca943a67504c63da00cfdc45131ae4b222d8d Mon Sep 17 00:00:00 2001 From: Moshe Atlow Date: Sun, 20 Feb 2022 10:58:05 +0200 Subject: [PATCH] feat: pass rawHeaders (#1240) * Pass raw headers * tests * types * CR Co-authored-by: Moshe Atlow --- lib/api/api-connect.js | 8 +++++--- lib/api/api-pipeline.js | 11 +++++++---- lib/api/api-request.js | 13 ++++++++----- lib/api/api-stream.js | 11 +++++++---- lib/api/api-upgrade.js | 8 +++++--- lib/core/util.js | 5 +++++ test/client-request.js | 37 +++++++++++++++++++++++++++++++++++++ test/util.js | 13 +++++++++++++ types/dispatcher.d.ts | 6 ++++++ 9 files changed, 93 insertions(+), 19 deletions(-) diff --git a/lib/api/api-connect.js b/lib/api/api-connect.js index cd97d1c61fb..0503b1a2f0e 100644 --- a/lib/api/api-connect.js +++ b/lib/api/api-connect.js @@ -15,7 +15,7 @@ class ConnectHandler extends AsyncResource { throw new InvalidArgumentError('invalid callback') } - const { signal, opaque } = opts + const { signal, opaque, responseHeaders } = opts if (signal && typeof signal.on !== 'function' && typeof signal.addEventListener !== 'function') { throw new InvalidArgumentError('signal must be an EventEmitter or EventTarget') @@ -24,6 +24,7 @@ class ConnectHandler extends AsyncResource { super('UNDICI_CONNECT') this.opaque = opaque || null + this.responseHeaders = responseHeaders || null this.callback = callback this.abort = null @@ -43,15 +44,16 @@ class ConnectHandler extends AsyncResource { throw new SocketError('bad connect', null) } - onUpgrade (statusCode, headers, socket) { + onUpgrade (statusCode, rawHeaders, socket) { const { callback, opaque, context } = this removeSignal(this) this.callback = null + const headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders) this.runInAsyncScope(callback, null, null, { statusCode, - headers: util.parseHeaders(headers), + headers, socket, opaque, context diff --git a/lib/api/api-pipeline.js b/lib/api/api-pipeline.js index b6949e62d94..af4a1803b44 100644 --- a/lib/api/api-pipeline.js +++ b/lib/api/api-pipeline.js @@ -69,7 +69,7 @@ class PipelineHandler extends AsyncResource { throw new InvalidArgumentError('invalid handler') } - const { signal, method, opaque, onInfo } = opts + const { signal, method, opaque, onInfo, responseHeaders } = opts if (signal && typeof signal.on !== 'function' && typeof signal.addEventListener !== 'function') { throw new InvalidArgumentError('signal must be an EventEmitter or EventTarget') @@ -86,6 +86,7 @@ class PipelineHandler extends AsyncResource { super('UNDICI_PIPELINE') this.opaque = opaque || null + this.responseHeaders = responseHeaders || null this.handler = handler this.abort = null this.context = null @@ -156,12 +157,13 @@ class PipelineHandler extends AsyncResource { this.context = context } - onHeaders (statusCode, headers, resume) { + onHeaders (statusCode, rawHeaders, resume) { const { opaque, handler, context } = this if (statusCode < 200) { if (this.onInfo) { - this.onInfo({ statusCode, headers: util.parseHeaders(headers) }) + const headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders) + this.onInfo({ statusCode, headers }) } return } @@ -171,9 +173,10 @@ class PipelineHandler extends AsyncResource { let body try { this.handler = null + const headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders) body = this.runInAsyncScope(handler, null, { statusCode, - headers: util.parseHeaders(headers), + headers, opaque, body: this.res, context diff --git a/lib/api/api-request.js b/lib/api/api-request.js index c8474f8e74a..2d5bcf54c6d 100644 --- a/lib/api/api-request.js +++ b/lib/api/api-request.js @@ -15,7 +15,7 @@ class RequestHandler extends AsyncResource { throw new InvalidArgumentError('invalid opts') } - const { signal, method, opaque, body, onInfo } = opts + const { signal, method, opaque, body, onInfo, responseHeaders } = opts try { if (typeof callback !== 'function') { @@ -42,6 +42,7 @@ class RequestHandler extends AsyncResource { throw err } + this.responseHeaders = responseHeaders || null this.opaque = opaque || null this.callback = callback this.res = null @@ -69,25 +70,27 @@ class RequestHandler extends AsyncResource { this.context = context } - onHeaders (statusCode, headers, resume) { + onHeaders (statusCode, rawHeaders, resume) { const { callback, opaque, abort, context } = this if (statusCode < 200) { if (this.onInfo) { - this.onInfo({ statusCode, headers: util.parseHeaders(headers) }) + const headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders) + this.onInfo({ statusCode, headers }) } return } - const parsedHeaders = util.parseHeaders(headers) + const parsedHeaders = util.parseHeaders(rawHeaders) const body = new Readable(resume, abort, parsedHeaders['content-type']) this.callback = null this.res = body + const headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders) this.runInAsyncScope(callback, null, null, { statusCode, - headers: parsedHeaders, + headers, trailers: this.trailers, opaque, body, diff --git a/lib/api/api-stream.js b/lib/api/api-stream.js index cc5901f7c4a..020fd08c586 100644 --- a/lib/api/api-stream.js +++ b/lib/api/api-stream.js @@ -16,7 +16,7 @@ class StreamHandler extends AsyncResource { throw new InvalidArgumentError('invalid opts') } - const { signal, method, opaque, body, onInfo } = opts + const { signal, method, opaque, body, onInfo, responseHeaders } = opts try { if (typeof callback !== 'function') { @@ -47,6 +47,7 @@ class StreamHandler extends AsyncResource { throw err } + this.responseHeaders = responseHeaders || null this.opaque = opaque || null this.factory = factory this.callback = callback @@ -75,20 +76,22 @@ class StreamHandler extends AsyncResource { this.context = context } - onHeaders (statusCode, headers, resume) { + onHeaders (statusCode, rawHeaders, resume) { const { factory, opaque, context } = this if (statusCode < 200) { if (this.onInfo) { - this.onInfo({ statusCode, headers: util.parseHeaders(headers) }) + const headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders) + this.onInfo({ statusCode, headers }) } return } this.factory = null + const headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders) const res = this.runInAsyncScope(factory, null, { statusCode, - headers: util.parseHeaders(headers), + headers, opaque, context }) diff --git a/lib/api/api-upgrade.js b/lib/api/api-upgrade.js index f40fe15c0b3..ef783e82975 100644 --- a/lib/api/api-upgrade.js +++ b/lib/api/api-upgrade.js @@ -16,7 +16,7 @@ class UpgradeHandler extends AsyncResource { throw new InvalidArgumentError('invalid callback') } - const { signal, opaque } = opts + const { signal, opaque, responseHeaders } = opts if (signal && typeof signal.on !== 'function' && typeof signal.addEventListener !== 'function') { throw new InvalidArgumentError('signal must be an EventEmitter or EventTarget') @@ -24,6 +24,7 @@ class UpgradeHandler extends AsyncResource { super('UNDICI_UPGRADE') + this.responseHeaders = responseHeaders || null this.opaque = opaque || null this.callback = callback this.abort = null @@ -45,7 +46,7 @@ class UpgradeHandler extends AsyncResource { throw new SocketError('bad upgrade', null) } - onUpgrade (statusCode, headers, socket) { + onUpgrade (statusCode, rawHeaders, socket) { const { callback, opaque, context } = this assert.strictEqual(statusCode, 101) @@ -53,8 +54,9 @@ class UpgradeHandler extends AsyncResource { removeSignal(this) this.callback = null + const headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders) this.runInAsyncScope(callback, null, null, { - headers: util.parseHeaders(headers), + headers, socket, opaque, context diff --git a/lib/core/util.js b/lib/core/util.js index e624c097ddf..7b32f8d943a 100644 --- a/lib/core/util.js +++ b/lib/core/util.js @@ -200,6 +200,10 @@ function parseHeaders (headers, obj = {}) { return obj } +function parseRawHeaders (headers) { + return headers.map(header => header.toString()); +} + function isBuffer (buffer) { // See, https://github.com/mcollina/undici/pull/319 return buffer instanceof Uint8Array || Buffer.isBuffer(buffer) @@ -339,6 +343,7 @@ module.exports = { isIterable, isAsyncIterable, isDestroyed, + parseRawHeaders, parseHeaders, parseKeepAliveTimeout, destroy, diff --git a/test/client-request.js b/test/client-request.js index e0b6f315a72..781559a0c62 100644 --- a/test/client-request.js +++ b/test/client-request.js @@ -537,6 +537,43 @@ test('request onInfo callback headers parsing', async (t) => { t.pass() }) + +test('request raw responseHeaders', async (t) => { + t.plan(4) + const infos = [] + + const server = net.createServer((socket) => { + const lines = [ + 'HTTP/1.1 103 Early Hints', + 'Link: ; rel=preload; as=style', + '', + 'HTTP/1.1 200 OK', + 'Date: Sat, 09 Oct 2010 14:28:02 GMT', + 'Connection: close', + '', + 'the body' + ] + socket.end(lines.join('\r\n')) + }) + t.teardown(server.close.bind(server)) + + await promisify(server.listen.bind(server))(0) + + const client = new Client(`http://localhost:${server.address().port}`) + t.teardown(client.close.bind(client)) + + const { headers } = await client.request({ + path: '/', + method: 'GET', + responseHeaders: 'raw', + onInfo: (x) => { infos.push(x) } + }) + t.equal(infos.length, 1) + t.same(infos[0].headers, ['Link', '; rel=preload; as=style']) + t.same(headers, ['Date', 'Sat, 09 Oct 2010 14:28:02 GMT', 'Connection', 'close']) + t.pass() +}) + test('request formData', { skip: nodeMajor < 16 }, (t) => { t.plan(1) diff --git a/test/util.js b/test/util.js index 2231128181a..f286081e3a1 100644 --- a/test/util.js +++ b/test/util.js @@ -81,3 +81,16 @@ test('validateHandler', (t) => { onUpgrade: 'null' }, 'CONNECT', () => {}), InvalidArgumentError, 'invalid onUpgrade method') }) + +test('parseHeaders', (t) => { + t.plan(4) + t.same(util.parseHeaders(['key', 'value']), { key: 'value' }) + t.same(util.parseHeaders([Buffer.from('key'), Buffer.from('value')]), { key: 'value' }) + t.same(util.parseHeaders(['Key', 'Value']), { key: 'Value' }) + t.same(util.parseHeaders(['Key', 'value', 'key', 'Value']), { key: ['value', 'Value'] }) +}) + +test('parseRawHeaders', (t) => { + t.plan(1) + t.same(util.parseRawHeaders(['key', 'value', Buffer.from('key'), Buffer.from('value')]), ['key', 'value', 'key', 'value']) +}) \ No newline at end of file diff --git a/types/dispatcher.d.ts b/types/dispatcher.d.ts index 7e8cd18c40c..fefe6d150e6 100644 --- a/types/dispatcher.d.ts +++ b/types/dispatcher.d.ts @@ -64,6 +64,8 @@ declare namespace Dispatcher { opaque?: unknown; /** Default: 0 */ maxRedirections?: number; + /** Default: `null` */ + responseHeader?: 'raw' | null; } export interface RequestOptions extends DispatchOptions { /** Default: `null` */ @@ -74,6 +76,8 @@ declare namespace Dispatcher { maxRedirections?: number; /** Default: `null` */ onInfo?: (info: {statusCode: number, headers: Record}) => void; + /** Default: `null` */ + responseHeader?: 'raw' | null; } export interface PipelineOptions extends RequestOptions { /** `true` if the `handler` will return an object stream. Default: `false` */ @@ -91,6 +95,8 @@ declare namespace Dispatcher { signal?: AbortSignal | EventEmitter | null; /** Default: 0 */ maxRedirections?: number; + /** Default: `null` */ + responseHeader?: 'raw' | null; } export interface ConnectData { statusCode: number;