Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add rawHeaders to IncomingMessage #31853

Merged
merged 7 commits into from Jan 24, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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.
nornagon marked this conversation as resolved.
Show resolved Hide resolved

```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 () {
MRayermannMSFT marked this conversation as resolved.
Show resolved Hide resolved
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;
MRayermannMSFT marked this conversation as resolved.
Show resolved Hide resolved
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