Browse Source

feat: add rawHeaders to IncomingMessage (#31853)

* 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 #
Matthew Rayermann 3 years ago
parent
commit
d26d337bb8

+ 22 - 0
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)
+```

+ 19 - 9
lib/browser/api/net.ts

@@ -61,24 +61,25 @@ 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;
           }
         }
       }
@@ -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}`;
   }

+ 1 - 1
shell/browser/api/electron_api_url_loader.cc

@@ -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();
   }

+ 137 - 1
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;