Skip to content

Commit

Permalink
fix: regression regarding Headers.plain(); introduced Headers.raw() (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
stefan-guggisberg authored Oct 14, 2022
1 parent 0073bd8 commit 2a05a77
Show file tree
Hide file tree
Showing 9 changed files with 55 additions and 26 deletions.
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
* The following `fetch()` options are ignored due to the nature of Node.js and since `@adobe/fetch` doesn't have the concept of web pages: `mode`, `referrer`, `referrerPolicy`, `integrity` and `credentials`.
* The `fetch()` option `keepalive` is not supported. But you can use the `h1.keepAlive` context option, as demonstrated [here](#http11-keep-alive).

`@adobe/fetch` also supports the following extensions:
`@adobe/fetch` also supports the following non-spec extensions:

* `Response.buffer()` returns a Node.js `Buffer`.
* `Response.url` contains the final url when following redirects.
Expand All @@ -76,6 +76,7 @@
* The `Response` object has an extra property `fromCache` which determines whether the response was retrieved from cache.
* The `Response` object has an extra property `decoded` which determines whether the response body was automatically decoded (see Fetch option `decode` below).
* `Response.headers.plain()` returns the headers as a plain object.
* `Response.headers.raw()` returns the internal/raw representation of the headers where e.g. the `Set-Cokkie` header is represented with an array of strings value.
* The Fetch option `follow` allows to limit the number of redirects to follow (default: `20`).
* The Fetch option `compress` enables transparent gzip/deflate/br content encoding (default: `true`).
* The Fetch option `decode` enables transparent gzip/deflate/br content decoding (default: `true`).
Expand Down Expand Up @@ -113,7 +114,7 @@ Apart from the standard Fetch API
* `Headers`
* `Body`

`@adobe/fetch` exposes the following extensions:
`@adobe/fetch` exposes the following non-spec extensions:

* `context()` - creates a new customized API context
* `reset()` - resets the current API context, i.e. closes pending sessions/sockets, clears internal caches, etc ...
Expand Down Expand Up @@ -268,7 +269,7 @@ interface Http2Options {

### Specify a timeout for a `fetch` operation

Using `timeoutSignal(ms)` extension:
Using `timeoutSignal(ms)` non-spec extension:

```javascript
const { fetch, timeoutSignal, AbortError } = require('@adobe/fetch');
Expand Down Expand Up @@ -458,14 +459,14 @@ console.log(`Connection: ${resp.headers.get('connection')}`); // -> keep-alive

### Extract Set-Cookie Header

Unlike browsers, you can access raw `Set-Cookie` headers manually using `Headers.plain()`. This is an `@adobe/fetch` only API.
Unlike browsers, you can access raw `Set-Cookie` headers manually using `Headers.raw()`. This is an `@adobe/fetch` only API.

```javascript
const { fetch } = require('@adobe/fetch');

const resp = await fetch('https://httpbin.org/cookies/set?a=1&b=2');
// returns an array of values, instead of a string of comma-separated values
console.log(resp.headers.plain()['set-cookie']);
console.log(resp.headers.raw()['set-cookie']);
```

### Self-signed Certificates
Expand Down
4 changes: 2 additions & 2 deletions src/api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ export class Response extends Body {
readonly decoded: boolean;
headers: Headers;

// extensions
// non-spec extensions
/**
* A boolean specifying whether the response was retrieved from the cache.
*/
Expand Down Expand Up @@ -262,7 +262,7 @@ export interface RequestOptions {
*/
signal?: AbortSignal;

// extensions
// non-spec extensions
/**
* A boolean specifying support of gzip/deflate/brotli content encoding.
* @default true
Expand Down
2 changes: 1 addition & 1 deletion src/fetch/body.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ class Body {

/**
* Consume the body and return a promise that will resolve to a Node.js Buffer.
* (extension)
* (non-spec extension)
*
* @return {Promise<Buffer>}
*/
Expand Down
24 changes: 20 additions & 4 deletions src/fetch/headers.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,8 @@ class Headers {
};

if (init instanceof Headers) {
init.forEach((value, name) => {
this.append(name, value);
init[INTERNALS].map.forEach((value, name) => {
this[INTERNALS].map.set(name, Array.isArray(value) ? [...value] : value);
});
} else if (Array.isArray(init)) {
init.forEach(([name, value]) => {
Expand Down Expand Up @@ -177,11 +177,27 @@ class Headers {

/**
* Returns the headers as a plain object.
* (extension)
* (non-spec extension)
*
* @return {object}
* @returns {Record<string, string>}
*/
plain() {
return [...this.keys()].reduce((result, key) => {
// eslint-disable-next-line no-param-reassign
result[key] = this.get(key);
return result;
}, {});
}

/**
* Returns the internal/raw representation of the
* headers, i.e. the value of an multi-valued header
* (added with <code>append()</code>) is an array of strings.
* (non-spec extension)
*
* @returns {Record<string, string|string[]>}
*/
raw() {
return Object.fromEntries(this[INTERNALS].map);
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/fetch/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,7 @@ class FetchContext {
AbortController,
AbortSignal,

// extensions
// non-spec extensions

FetchBaseError,
FetchError,
Expand Down
4 changes: 2 additions & 2 deletions src/fetch/request.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ class Request extends Body {

if (!headers.has('content-type')) {
if (isPlainObject(body)) {
// extension: support plain js object body (JSON serialization)
// non-spec extension: support plain js object body (JSON serialization)
body = JSON.stringify(body);
headers.set('content-type', 'application/json');
} else {
Expand Down Expand Up @@ -114,7 +114,7 @@ class Request extends Body {
signal,
};

// extension options
// non-spec extension options
if (init.follow === undefined) {
if (!req || req.follow === undefined) {
this.follow = DEFAULT_FOLLOW;
Expand Down
6 changes: 3 additions & 3 deletions src/fetch/response.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ class Response extends Body {

if (respBody !== null && !headers.has('content-type')) {
if (isPlainObject(respBody)) {
// extension: support plain js object body (JSON serialization)
// non-spec extension: support plain js object body (JSON serialization)
respBody = JSON.stringify(respBody);
headers.set('content-type', 'application/json');
} else {
Expand Down Expand Up @@ -104,12 +104,12 @@ class Response extends Body {
return this[INTERNALS].headers;
}

// extension
// non-spec extension
get httpVersion() {
return this[INTERNALS].httpVersion;
}

// extension
// non-spec extension
get decoded() {
return this[INTERNALS].decoded;
}
Expand Down
24 changes: 18 additions & 6 deletions test/fetch/headers.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -186,14 +186,26 @@ describe('Headers Tests', () => {
it('plain() should return plain object representation', () => {
const hdrObj = { foo: 'bar', 'x-cookies': ['a=1', 'b=2'] };
const headers = new Headers(hdrObj);
expect(headers.plain()).to.be.deep.equal(hdrObj);
expect(headers.plain()).to.be.deep.equal({ foo: 'bar', 'x-cookies': 'a=1, b=2' });
});

it('raw() should return multi-valued headers as array of strings', () => {
const hdrObj = { foo: 'bar', 'x-cookies': ['a=1', 'b=2'] };
const headers = new Headers(hdrObj);
expect(headers.raw()).to.be.deep.equal(hdrObj);
});

it('should support multi-valued headers (e.g. Set-Cookie)', () => {
const headers = new Headers();
headers.set('set-cookie', 'a=1; Path=/');
headers.append('set-cookie', 'b=2; Path=/');
expect(headers.get('set-cookie')).to.be.equal('a=1; Path=/, b=2; Path=/');
expect(headers.plain()['set-cookie']).to.be.deep.equal(['a=1; Path=/', 'b=2; Path=/']);
let headers = new Headers();
headers.append('set-cookie', 't=1; Secure');
headers.append('set-cookie', 'u=2; Secure');
expect(headers.get('set-cookie')).to.be.equal('t=1; Secure, u=2; Secure');
expect(headers.plain()['set-cookie']).to.be.deep.equal('t=1; Secure, u=2; Secure');
expect(headers.raw()['set-cookie']).to.be.deep.equal(['t=1; Secure', 'u=2; Secure']);

headers = new Headers(headers);
expect(headers.get('set-cookie')).to.be.equal('t=1; Secure, u=2; Secure');
expect(headers.plain()['set-cookie']).to.be.deep.equal('t=1; Secure, u=2; Secure');
expect(headers.raw()['set-cookie']).to.be.deep.equal(['t=1; Secure', 'u=2; Secure']);
});
});
4 changes: 2 additions & 2 deletions test/fetch/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -608,8 +608,8 @@ testParams.forEach((params) => {
// set-cookie: b=2; [Secure; ]Path=/
assert.strictEqual(resp.status, 302);
assert(/a=1; (Secure; )?Path=\/, b=2; (Secure; )?Path=\//.test(resp.headers.get('set-cookie')));
assert(/a=1; (Secure; )?Path=\//.test(resp.headers.plain()['set-cookie'][0]));
assert(/b=2; (Secure; )?Path=\//.test(resp.headers.plain()['set-cookie'][1]));
assert(/a=1; (Secure; )?Path=\//.test(resp.headers.raw()['set-cookie'][0]));
assert(/b=2; (Secure; )?Path=\//.test(resp.headers.raw()['set-cookie'][1]));
});

if (protocol === 'https') {
Expand Down

0 comments on commit 2a05a77

Please sign in to comment.