diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9cebbb20bef..a23d5edec71 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -48,6 +48,6 @@ jobs: - name: Coverage Report if: inputs.runs-on == 'ubuntu-latest' && inputs.node-version == 20 - uses: codecov/codecov-action@54bcd8715eee62d40e33596ef5e8f0f48dbbccab # v4.1.0 + uses: codecov/codecov-action@c16abc29c95fcf9174b58eb7e1abf4c866893bc8 # v4.1.1 with: token: ${{ secrets.CODECOV_TOKEN }} diff --git a/docs/docs/api/DiagnosticsChannel.md b/docs/docs/api/DiagnosticsChannel.md index 099c072f6c6..cb0421d84d4 100644 --- a/docs/docs/api/DiagnosticsChannel.md +++ b/docs/docs/api/DiagnosticsChannel.md @@ -20,8 +20,6 @@ diagnosticsChannel.channel('undici:request:create').subscribe(({ request }) => { console.log('method', request.method) console.log('path', request.path) console.log('headers') // array of strings, e.g: ['foo', 'bar'] - request.addHeader('hello', 'world') - console.log('headers', request.headers) // e.g. ['foo', 'bar', 'hello', 'world'] }) ``` diff --git a/lib/core/request.js b/lib/core/request.js index 37839d3c949..6d9ebc8bc8f 100644 --- a/lib/core/request.js +++ b/lib/core/request.js @@ -91,6 +91,8 @@ class Request { this.abort = null + this.publicInterface = null + if (body == null) { this.body = null } else if (isStream(body)) { @@ -187,10 +189,32 @@ class Request { this[kHandler] = handler if (channels.create.hasSubscribers) { - channels.create.publish({ request: this }) + channels.create.publish({ request: this.getPublicInterface() }) } } + getPublicInterface () { + const self = this + this.publicInterface ??= { + get origin () { + return self.origin + }, + get method () { + return self.method + }, + get path () { + return self.path + }, + get headers () { + return self.headers + }, + get completed () { + return self.completed + } + } + return this.publicInterface + } + onBodySent (chunk) { if (this[kHandler].onBodySent) { try { @@ -203,7 +227,7 @@ class Request { onRequestSent () { if (channels.bodySent.hasSubscribers) { - channels.bodySent.publish({ request: this }) + channels.bodySent.publish({ request: this.getPublicInterface() }) } if (this[kHandler].onRequestSent) { @@ -236,7 +260,7 @@ class Request { assert(!this.completed) if (channels.headers.hasSubscribers) { - channels.headers.publish({ request: this, response: { statusCode, headers, statusText } }) + channels.headers.publish({ request: this.getPublicInterface(), response: { statusCode, headers, statusText } }) } try { @@ -272,7 +296,7 @@ class Request { this.completed = true if (channels.trailers.hasSubscribers) { - channels.trailers.publish({ request: this, trailers }) + channels.trailers.publish({ request: this.getPublicInterface(), trailers }) } try { @@ -287,7 +311,7 @@ class Request { this.onFinally() if (channels.error.hasSubscribers) { - channels.error.publish({ request: this, error }) + channels.error.publish({ request: this.getPublicInterface(), error }) } if (this.aborted) { @@ -309,11 +333,6 @@ class Request { this.endHandler = null } } - - addHeader (key, value) { - processHeader(this, key, value) - return this - } } function processHeader (request, key, val) { diff --git a/lib/core/util.js b/lib/core/util.js index a62396e23e0..e7b5d9c1edd 100644 --- a/lib/core/util.js +++ b/lib/core/util.js @@ -246,9 +246,6 @@ function bufferToLowerCasedHeaderName (value) { * @returns {Record} */ function parseHeaders (headers, obj) { - // For H2 support - if (!Array.isArray(headers)) return headers - if (obj === undefined) obj = {} for (let i = 0; i < headers.length; i += 2) { const key = headerNameToString(headers[i]) diff --git a/lib/dispatcher/client-h1.js b/lib/dispatcher/client-h1.js index 62a3e29ef24..da70f7d8313 100644 --- a/lib/dispatcher/client-h1.js +++ b/lib/dispatcher/client-h1.js @@ -993,7 +993,7 @@ function writeH1 (client, request) { } if (channels.sendHeaders.hasSubscribers) { - channels.sendHeaders.publish({ request, headers: header, socket }) + channels.sendHeaders.publish({ request: request.getPublicInterface(), headers: header, socket }) } /* istanbul ignore else: assertion */ diff --git a/lib/dispatcher/client-h2.js b/lib/dispatcher/client-h2.js index d593eae4fca..0a53b75e501 100644 --- a/lib/dispatcher/client-h2.js +++ b/lib/dispatcher/client-h2.js @@ -54,6 +54,20 @@ const { } } = http2 +function parseH2Headers (headers) { + // set-cookie is always an array. Duplicates are added to the array. + // For duplicate cookie headers, the values are joined together with '; '. + headers = Object.entries(headers).flat(2) + + const result = [] + + for (const header of headers) { + result.push(Buffer.from(header)) + } + + return result +} + async function connectH2 (client, socket) { client[kSocket] = socket @@ -391,7 +405,7 @@ function writeH2 (client, request) { const { [HTTP2_HEADER_STATUS]: statusCode, ...realHeaders } = headers request.onResponseStarted() - if (request.onHeaders(Number(statusCode), realHeaders, stream.resume.bind(stream), '') === false) { + if (request.onHeaders(Number(statusCode), parseH2Headers(realHeaders), stream.resume.bind(stream), '') === false) { stream.pause() } diff --git a/lib/web/fetch/index.js b/lib/web/fetch/index.js index d8c20c59bf7..58d61585c06 100644 --- a/lib/web/fetch/index.js +++ b/lib/web/fetch/index.js @@ -2141,29 +2141,6 @@ async function httpNetworkFetch ( codings = contentEncoding.toLowerCase().split(',').map((x) => x.trim()) } location = headersList.get('location', true) - } else { - const keys = Object.keys(rawHeaders) - for (let i = 0; i < keys.length; ++i) { - // The header names are already in lowercase. - const key = keys[i] - const value = rawHeaders[key] - if (key === 'set-cookie') { - for (let j = 0; j < value.length; ++j) { - headersList.append(key, value[j], true) - } - } else { - headersList.append(key, value, true) - } - } - // For H2, The header names are already in lowercase, - // so we can avoid the `HeadersList#get` call here. - const contentEncoding = rawHeaders['content-encoding'] - if (contentEncoding) { - // https://www.rfc-editor.org/rfc/rfc7231#section-3.1.2.1 - // "All content-coding values are case-insensitive..." - codings = contentEncoding.toLowerCase().split(',').map((x) => x.trim()).reverse() - } - location = rawHeaders.location } this.body = new Readable({ read: resume }) diff --git a/package.json b/package.json index f7d7bfd5b9a..a3dc801c4dc 100644 --- a/package.json +++ b/package.json @@ -96,7 +96,7 @@ "@sinonjs/fake-timers": "^11.1.0", "@types/node": "^18.0.3", "abort-controller": "^3.0.0", - "borp": "^0.9.1", + "borp": "^0.10.0", "c8": "^9.1.0", "cross-env": "^7.0.3", "dns-packet": "^5.4.0", @@ -112,7 +112,7 @@ "proxy": "^2.1.1", "snazzy": "^9.0.0", "standard": "^17.0.0", - "tsd": "^0.30.1", + "tsd": "^0.31.0", "typescript": "^5.0.2", "ws": "^8.11.0" }, diff --git a/test/fetch/http2.js b/test/fetch/http2.js index 0793a21556c..1cef1d00ce6 100644 --- a/test/fetch/http2.js +++ b/test/fetch/http2.js @@ -34,7 +34,7 @@ test('[Fetch] Issue#2311', async (t) => { res.end(body) }) - const { strictEqual } = tspl(t, { plan: 1 }) + const { strictEqual } = tspl(t, { plan: 2 }) server.listen() await once(server, 'listening') @@ -65,6 +65,7 @@ test('[Fetch] Issue#2311', async (t) => { t.after(closeClientAndServerAsPromise(client, server)) strictEqual(responseBody, expectedBody) + strictEqual(response.headers.get('x-custom-h2'), 'foo') }) test('[Fetch] Simple GET with h2', async (t) => { diff --git a/test/http2.js b/test/http2.js index 3319ffddb1e..849a0cc43ba 100644 --- a/test/http2.js +++ b/test/http2.js @@ -851,8 +851,10 @@ test('Should handle h2 request with body (string or buffer) - dispatch', async t }, onHeaders (statusCode, headers) { t.strictEqual(statusCode, 200) - t.strictEqual(headers['content-type'], 'text/plain; charset=utf-8') - t.strictEqual(headers['x-custom-h2'], 'foo') + t.strictEqual(headers[0].toString('utf-8'), 'content-type') + t.strictEqual(headers[1].toString('utf-8'), 'text/plain; charset=utf-8') + t.strictEqual(headers[2].toString('utf-8'), 'x-custom-h2') + t.strictEqual(headers[3].toString('utf-8'), 'foo') }, onData (chunk) { response.push(chunk) diff --git a/test/node-test/diagnostics-channel/get.js b/test/node-test/diagnostics-channel/get.js index 397dfa3bc5f..261c136f740 100644 --- a/test/node-test/diagnostics-channel/get.js +++ b/test/node-test/diagnostics-channel/get.js @@ -7,7 +7,7 @@ const { Client } = require('../../..') const { createServer } = require('node:http') test('Diagnostics channel - get', (t) => { - const assert = tspl(t, { plan: 32 }) + const assert = tspl(t, { plan: 31 }) const server = createServer((req, res) => { res.setHeader('Content-Type', 'text/plain') res.setHeader('trailer', 'foo') @@ -33,8 +33,6 @@ test('Diagnostics channel - get', (t) => { assert.equal(request.method, 'GET') assert.equal(request.path, '/') assert.deepStrictEqual(request.headers, ['bar', 'bar']) - request.addHeader('hello', 'world') - assert.deepStrictEqual(request.headers, ['bar', 'bar', 'hello', 'world']) }) let _connector @@ -77,8 +75,7 @@ test('Diagnostics channel - get', (t) => { 'GET / HTTP/1.1', `host: localhost:${server.address().port}`, 'connection: keep-alive', - 'bar: bar', - 'hello: world' + 'bar: bar' ] assert.deepStrictEqual(headers, expectedHeaders.join('\r\n') + '\r\n') diff --git a/test/node-test/diagnostics-channel/post-stream.js b/test/node-test/diagnostics-channel/post-stream.js index 881873a7c1c..9cc5540290b 100644 --- a/test/node-test/diagnostics-channel/post-stream.js +++ b/test/node-test/diagnostics-channel/post-stream.js @@ -8,7 +8,7 @@ const { Client } = require('../../..') const { createServer } = require('node:http') test('Diagnostics channel - post stream', (t) => { - const assert = tspl(t, { plan: 33 }) + const assert = tspl(t, { plan: 31 }) const server = createServer((req, res) => { req.resume() res.setHeader('Content-Type', 'text/plain') @@ -34,9 +34,6 @@ test('Diagnostics channel - post stream', (t) => { assert.equal(request.method, 'POST') assert.equal(request.path, '/') assert.deepStrictEqual(request.headers, ['bar', 'bar']) - request.addHeader('hello', 'world') - assert.deepStrictEqual(request.headers, ['bar', 'bar', 'hello', 'world']) - assert.deepStrictEqual(request.body, body) }) let _connector @@ -79,8 +76,7 @@ test('Diagnostics channel - post stream', (t) => { 'POST / HTTP/1.1', `host: localhost:${server.address().port}`, 'connection: keep-alive', - 'bar: bar', - 'hello: world' + 'bar: bar' ] assert.equal(headers, expectedHeaders.join('\r\n') + '\r\n') diff --git a/test/node-test/diagnostics-channel/post.js b/test/node-test/diagnostics-channel/post.js index 1408ffbf023..06c56b673ac 100644 --- a/test/node-test/diagnostics-channel/post.js +++ b/test/node-test/diagnostics-channel/post.js @@ -7,7 +7,7 @@ const { Client } = require('../../../') const { createServer } = require('node:http') test('Diagnostics channel - post', (t) => { - const assert = tspl(t, { plan: 33 }) + const assert = tspl(t, { plan: 31 }) const server = createServer((req, res) => { req.resume() res.setHeader('Content-Type', 'text/plain') @@ -32,9 +32,6 @@ test('Diagnostics channel - post', (t) => { assert.equal(request.method, 'POST') assert.equal(request.path, '/') assert.deepStrictEqual(request.headers, ['bar', 'bar']) - request.addHeader('hello', 'world') - assert.deepStrictEqual(request.headers, ['bar', 'bar', 'hello', 'world']) - assert.deepStrictEqual(request.body, Buffer.from('hello world')) }) let _connector @@ -77,8 +74,7 @@ test('Diagnostics channel - post', (t) => { 'POST / HTTP/1.1', `host: localhost:${server.address().port}`, 'connection: keep-alive', - 'bar: bar', - 'hello: world' + 'bar: bar' ] assert.deepStrictEqual(headers, expectedHeaders.join('\r\n') + '\r\n') diff --git a/types/diagnostics-channel.d.ts b/types/diagnostics-channel.d.ts index 85d44823978..a037d1e0b2c 100644 --- a/types/diagnostics-channel.d.ts +++ b/types/diagnostics-channel.d.ts @@ -9,8 +9,7 @@ declare namespace DiagnosticsChannel { completed: boolean; method?: Dispatcher.HttpMethod; path: string; - headers: string; - addHeader(key: string, value: string): Request; + headers: any; } interface Response { statusCode: number;