Skip to content

Commit

Permalink
feat: add rawHeaders to IncomingMessage (#31853)
Browse files Browse the repository at this point in the history
* Add response.rawHeaders to docs for IncomingMessage

* Remove trailing spaces

* Implement raw headers, add tests

* Fix lint issues

* Add example from NodeJS docs

* Fix lint issue in doc example

* Add missing #
  • Loading branch information
MRayermannMSFT committed Jan 24, 2022
1 parent d1b48c0 commit d26d337
Show file tree
Hide file tree
Showing 4 changed files with 179 additions and 11 deletions.
22 changes: 22 additions & 0 deletions docs/api/incoming-message.md
Expand Up @@ -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)
```
28 changes: 19 additions & 9 deletions lib/browser/api/net.ts
Expand Up @@ -61,31 +61,41 @@ class IncomingMessage extends Readable {
const filteredHeaders: Record<string, string | string[]> = {};
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;
}
}
}
});
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}`;
}
Expand Down
2 changes: 1 addition & 1 deletion shell/browser/api/electron_api_url_loader.cc
Expand Up @@ -40,7 +40,7 @@ struct Converter<network::mojom::HttpRawHeaderPairPtr> {
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();
}
Expand Down
138 changes: 137 additions & 1 deletion spec-main/api-net-spec.ts
Expand Up @@ -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);

Expand Down Expand Up @@ -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';
Expand All @@ -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;
Expand Down

0 comments on commit d26d337

Please sign in to comment.