diff --git a/docs/api/incoming-message.md b/docs/api/incoming-message.md index 424c4b45fe3..92b9dec58f1 100644 --- a/docs/api/incoming-message.md +++ b/docs/api/incoming-message.md @@ -80,3 +80,25 @@ An `Integer` indicating the HTTP protocol major version number. An `Integer` indicating the HTTP protocol minor version number. [event-emitter]: https://nodejs.org/api/events.html#events_class_eventemitter + +#### `response.rawHeaders` + +A `string[]` containing the raw HTTP response headers exactly as they were +received. The keys and values are in the same list. It is not a list of +tuples. So, the even-numbered offsets are key values, and the odd-numbered +offsets are the associated values. Header names are not lowercased, and +duplicates are not merged. + +```javascript +// Prints something like: +// +// [ 'user-agent', +// 'this is invalid because there can be only one', +// 'User-Agent', +// 'curl/7.22.0', +// 'Host', +// '127.0.0.1:8000', +// 'ACCEPT', +// '*/*' ] +console.log(request.rawHeaders) +``` diff --git a/lib/browser/api/net.ts b/lib/browser/api/net.ts index a50348ac40f..454505ede04 100644 --- a/lib/browser/api/net.ts +++ b/lib/browser/api/net.ts @@ -61,24 +61,25 @@ class IncomingMessage extends Readable { const filteredHeaders: Record = {}; const { rawHeaders } = this._responseHead; rawHeaders.forEach(header => { - if (Object.prototype.hasOwnProperty.call(filteredHeaders, header.key) && - discardableDuplicateHeaders.has(header.key)) { + const keyLowerCase = header.key.toLowerCase(); + if (Object.prototype.hasOwnProperty.call(filteredHeaders, keyLowerCase) && + discardableDuplicateHeaders.has(keyLowerCase)) { // do nothing with discardable duplicate headers } else { - if (header.key === 'set-cookie') { + if (keyLowerCase === 'set-cookie') { // keep set-cookie as an array per Node.js rules // see https://nodejs.org/api/http.html#http_message_headers - if (Object.prototype.hasOwnProperty.call(filteredHeaders, header.key)) { - (filteredHeaders[header.key] as string[]).push(header.value); + if (Object.prototype.hasOwnProperty.call(filteredHeaders, keyLowerCase)) { + (filteredHeaders[keyLowerCase] as string[]).push(header.value); } else { - filteredHeaders[header.key] = [header.value]; + filteredHeaders[keyLowerCase] = [header.value]; } } else { // for non-cookie headers, the values are joined together with ', ' - if (Object.prototype.hasOwnProperty.call(filteredHeaders, header.key)) { - filteredHeaders[header.key] += `, ${header.value}`; + if (Object.prototype.hasOwnProperty.call(filteredHeaders, keyLowerCase)) { + filteredHeaders[keyLowerCase] += `, ${header.value}`; } else { - filteredHeaders[header.key] = header.value; + filteredHeaders[keyLowerCase] = header.value; } } } @@ -86,6 +87,15 @@ class IncomingMessage extends Readable { return filteredHeaders; } + get rawHeaders () { + const rawHeadersArr: string[] = []; + const { rawHeaders } = this._responseHead; + rawHeaders.forEach(header => { + rawHeadersArr.push(header.key, header.value); + }); + return rawHeadersArr; + } + get httpVersion () { return `${this.httpVersionMajor}.${this.httpVersionMinor}`; } diff --git a/shell/browser/api/electron_api_url_loader.cc b/shell/browser/api/electron_api_url_loader.cc index 800fd20ec5b..62675d2f3bf 100644 --- a/shell/browser/api/electron_api_url_loader.cc +++ b/shell/browser/api/electron_api_url_loader.cc @@ -40,7 +40,7 @@ struct Converter { v8::Isolate* isolate, const network::mojom::HttpRawHeaderPairPtr& pair) { gin_helper::Dictionary dict = gin::Dictionary::CreateEmpty(isolate); - dict.Set("key", base::ToLowerASCII(pair->key)); + dict.Set("key", pair->key); dict.Set("value", pair->value); return dict.GetHandle(); } diff --git a/spec-main/api-net-spec.ts b/spec-main/api-net-spec.ts index 44c05ea10be..2386aacaff8 100644 --- a/spec-main/api-net-spec.ts +++ b/spec-main/api-net-spec.ts @@ -1565,6 +1565,11 @@ describe('net module', () => { const headerValue = headers[customHeaderName.toLowerCase()]; expect(headerValue).to.equal(customHeaderValue); + const rawHeaders = response.rawHeaders; + expect(rawHeaders).to.be.an('array'); + expect(rawHeaders[0]).to.equal(customHeaderName); + expect(rawHeaders[1]).to.equal(customHeaderValue); + const httpVersion = response.httpVersion; expect(httpVersion).to.be.a('string').and.to.have.lengthOf.at.least(1); @@ -1606,7 +1611,7 @@ describe('net module', () => { await collectStreamBody(response); }); - it('should join repeated non-discardable value with ,', async () => { + it('should join repeated non-discardable header values with ,', async () => { const serverUrl = await respondOnce.toSingleURL((request, response) => { response.statusCode = 200; response.statusMessage = 'OK'; @@ -1626,6 +1631,137 @@ describe('net module', () => { await collectStreamBody(response); }); + it('should not join repeated discardable header values with ,', async () => { + const serverUrl = await respondOnce.toSingleURL((request, response) => { + response.statusCode = 200; + response.statusMessage = 'OK'; + response.setHeader('last-modified', ['yesterday', 'today']); + response.end(); + }); + const urlRequest = net.request(serverUrl); + const response = await getResponse(urlRequest); + expect(response.statusCode).to.equal(200); + expect(response.statusMessage).to.equal('OK'); + + const headers = response.headers; + expect(headers).to.be.an('object'); + expect(headers).to.have.property('last-modified'); + expect(headers['last-modified']).to.equal('yesterday'); + + await collectStreamBody(response); + }); + + it('should make set-cookie header an array even if single value', async () => { + const serverUrl = await respondOnce.toSingleURL((request, response) => { + response.statusCode = 200; + response.statusMessage = 'OK'; + response.setHeader('set-cookie', 'chocolate-chip'); + response.end(); + }); + const urlRequest = net.request(serverUrl); + const response = await getResponse(urlRequest); + expect(response.statusCode).to.equal(200); + expect(response.statusMessage).to.equal('OK'); + + const headers = response.headers; + expect(headers).to.be.an('object'); + expect(headers).to.have.property('set-cookie'); + expect(headers['set-cookie']).to.be.an('array'); + expect(headers['set-cookie'][0]).to.equal('chocolate-chip'); + + await collectStreamBody(response); + }); + + it('should keep set-cookie header an array when an array', async () => { + const serverUrl = await respondOnce.toSingleURL((request, response) => { + response.statusCode = 200; + response.statusMessage = 'OK'; + response.setHeader('set-cookie', ['chocolate-chip', 'oatmeal']); + response.end(); + }); + const urlRequest = net.request(serverUrl); + const response = await getResponse(urlRequest); + expect(response.statusCode).to.equal(200); + expect(response.statusMessage).to.equal('OK'); + + const headers = response.headers; + expect(headers).to.be.an('object'); + expect(headers).to.have.property('set-cookie'); + expect(headers['set-cookie']).to.be.an('array'); + expect(headers['set-cookie'][0]).to.equal('chocolate-chip'); + expect(headers['set-cookie'][1]).to.equal('oatmeal'); + + await collectStreamBody(response); + }); + + it('should lowercase header keys', async () => { + const serverUrl = await respondOnce.toSingleURL((request, response) => { + response.statusCode = 200; + response.statusMessage = 'OK'; + response.setHeader('HEADER-KEY', ['header-value']); + response.setHeader('SeT-CookiE', ['chocolate-chip', 'oatmeal']); + response.setHeader('rEFERREr-pOLICy', ['first-text', 'second-text']); + response.setHeader('LAST-modified', 'yesterday'); + + response.end(); + }); + const urlRequest = net.request(serverUrl); + const response = await getResponse(urlRequest); + expect(response.statusCode).to.equal(200); + expect(response.statusMessage).to.equal('OK'); + + const headers = response.headers; + expect(headers).to.be.an('object'); + + expect(headers).to.have.property('header-key'); + expect(headers).to.have.property('set-cookie'); + expect(headers).to.have.property('referrer-policy'); + expect(headers).to.have.property('last-modified'); + + await collectStreamBody(response); + }); + + it('should return correct raw headers', async () => { + const customHeaders: [string, string|string[]][] = [ + ['HEADER-KEY-ONE', 'header-value-one'], + ['set-cookie', 'chocolate-chip'], + ['header-key-two', 'header-value-two'], + ['referrer-policy', ['first-text', 'second-text']], + ['HEADER-KEY-THREE', 'header-value-three'], + ['last-modified', ['first-text', 'second-text']], + ['header-key-four', 'header-value-four'] + ]; + + const serverUrl = await respondOnce.toSingleURL((request, response) => { + response.statusCode = 200; + response.statusMessage = 'OK'; + customHeaders.forEach((headerTuple) => { + response.setHeader(headerTuple[0], headerTuple[1]); + }); + response.end(); + }); + const urlRequest = net.request(serverUrl); + const response = await getResponse(urlRequest); + expect(response.statusCode).to.equal(200); + expect(response.statusMessage).to.equal('OK'); + + const rawHeaders = response.rawHeaders; + expect(rawHeaders).to.be.an('array'); + + let rawHeadersIdx = 0; + customHeaders.forEach((headerTuple) => { + const headerKey = headerTuple[0]; + const headerValues = Array.isArray(headerTuple[1]) ? headerTuple[1] : [headerTuple[1]]; + headerValues.forEach((headerValue) => { + expect(rawHeaders[rawHeadersIdx]).to.equal(headerKey); + expect(rawHeaders[rawHeadersIdx + 1]).to.equal(headerValue); + rawHeadersIdx += 2; + }); + }); + + await collectStreamBody(response); + }); + it('should be able to pipe a net response into a writable stream', async () => { const bodyData = randomString(kOneKiloByte); let nodeRequestProcessed = false;