Skip to content

Commit

Permalink
Add support for Referrer and Referrer Policy (#1057)
Browse files Browse the repository at this point in the history
* Support referrer and referrerPolicy

* Test TS types for addition of referrer and referrerPolicy

* Fix lint issues and merge error
  • Loading branch information
tekwiz committed Nov 5, 2021
1 parent 0a67275 commit 2d80b0b
Show file tree
Hide file tree
Showing 7 changed files with 1,008 additions and 5 deletions.
17 changes: 17 additions & 0 deletions @types/index.d.ts
Expand Up @@ -71,6 +71,14 @@ export interface RequestInit {
* An AbortSignal to set request's signal.
*/
signal?: AbortSignal | null;
/**
* A string whose value is a same-origin URL, "about:client", or the empty string, to set request’s referrer.
*/
referrer?: string;
/**
* A referrer policy to set request’s referrerPolicy.
*/
referrerPolicy?: ReferrerPolicy;

// Node-fetch extensions to the whatwg/fetch spec
agent?: Agent | ((parsedUrl: URL) => Agent);
Expand Down Expand Up @@ -118,6 +126,7 @@ declare class BodyMixin {
export interface Body extends Pick<BodyMixin, keyof BodyMixin> {}

export type RequestRedirect = 'error' | 'follow' | 'manual';
export type ReferrerPolicy = '' | 'no-referrer' | 'no-referrer-when-downgrade' | 'same-origin' | 'origin' | 'strict-origin' | 'origin-when-cross-origin' | 'strict-origin-when-cross-origin' | 'unsafe-url';
export type RequestInfo = string | Request;
export class Request extends BodyMixin {
constructor(input: RequestInfo, init?: RequestInit);
Expand All @@ -142,6 +151,14 @@ export class Request extends BodyMixin {
* Returns the URL of request as a string.
*/
readonly url: string;
/**
* A string whose value is a same-origin URL, "about:client", or the empty string, to set request’s referrer.
*/
readonly referrer: string;
/**
* A referrer policy to set request’s referrerPolicy.
*/
readonly referrerPolicy: ReferrerPolicy;
clone(): Request;
}

Expand Down
2 changes: 0 additions & 2 deletions README.md
Expand Up @@ -581,8 +581,6 @@ Due to the nature of Node.js, the following properties are not implemented at th

- `type`
- `destination`
- `referrer`
- `referrerPolicy`
- `mode`
- `credentials`
- `cache`
Expand Down
11 changes: 10 additions & 1 deletion src/index.js
Expand Up @@ -19,6 +19,7 @@ import Request, {getNodeRequestOptions} from './request.js';
import {FetchError} from './errors/fetch-error.js';
import {AbortError} from './errors/abort-error.js';
import {isRedirect} from './utils/is-redirect.js';
import {parseReferrerPolicyFromHeader} from './utils/referrer.js';

export {Headers, Request, Response, FetchError, AbortError, isRedirect};

Expand Down Expand Up @@ -168,7 +169,9 @@ export default async function fetch(url, options_) {
method: request.method,
body: clone(request),
signal: request.signal,
size: request.size
size: request.size,
referrer: request.referrer,
referrerPolicy: request.referrerPolicy
};

// HTTP-redirect fetch step 9
Expand All @@ -185,6 +188,12 @@ export default async function fetch(url, options_) {
requestOptions.headers.delete('content-length');
}

// HTTP-redirect fetch step 14
const responseReferrerPolicy = parseReferrerPolicyFromHeader(headers);
if (responseReferrerPolicy) {
requestOptions.referrerPolicy = responseReferrerPolicy;
}

// HTTP-redirect fetch step 15
resolve(fetch(new Request(locationURL, requestOptions)));
finalize();
Expand Down
77 changes: 75 additions & 2 deletions src/request.js
Expand Up @@ -12,6 +12,9 @@ import Headers from './headers.js';
import Body, {clone, extractContentType, getTotalBytes} from './body.js';
import {isAbortSignal} from './utils/is.js';
import {getSearch} from './utils/get-search.js';
import {
validateReferrerPolicy, determineRequestsReferrer, DEFAULT_REFERRER_POLICY
} from './utils/referrer.js';

const INTERNALS = Symbol('Request internals');

Expand Down Expand Up @@ -93,12 +96,28 @@ export default class Request extends Body {
throw new TypeError('Expected signal to be an instanceof AbortSignal or EventTarget');
}

// §5.4, Request constructor steps, step 15.1
// eslint-disable-next-line no-eq-null, eqeqeq
let referrer = init.referrer == null ? input.referrer : init.referrer;
if (referrer === '') {
// §5.4, Request constructor steps, step 15.2
referrer = 'no-referrer';
} else if (referrer) {
// §5.4, Request constructor steps, step 15.3.1, 15.3.2
const parsedReferrer = new URL(referrer);
// §5.4, Request constructor steps, step 15.3.3, 15.3.4
referrer = /^about:(\/\/)?client$/.test(parsedReferrer) ? 'client' : parsedReferrer;
} else {
referrer = undefined;
}

this[INTERNALS] = {
method,
redirect: init.redirect || input.redirect || 'follow',
headers,
parsedURL,
signal
signal,
referrer
};

// Node-fetch-only options
Expand All @@ -108,6 +127,10 @@ export default class Request extends Body {
this.agent = init.agent || input.agent;
this.highWaterMark = init.highWaterMark || input.highWaterMark || 16384;
this.insecureHTTPParser = init.insecureHTTPParser || input.insecureHTTPParser || false;

// §5.4, Request constructor steps, step 16.
// Default is empty string per https://fetch.spec.whatwg.org/#concept-request-referrer-policy
this.referrerPolicy = init.referrerPolicy || input.referrerPolicy || '';
}

get method() {
Expand All @@ -130,6 +153,31 @@ export default class Request extends Body {
return this[INTERNALS].signal;
}

// https://fetch.spec.whatwg.org/#dom-request-referrer
get referrer() {
if (this[INTERNALS].referrer === 'no-referrer') {
return '';
}

if (this[INTERNALS].referrer === 'client') {
return 'about:client';
}

if (this[INTERNALS].referrer) {
return this[INTERNALS].referrer.toString();
}

return undefined;
}

get referrerPolicy() {
return this[INTERNALS].referrerPolicy;
}

set referrerPolicy(referrerPolicy) {
this[INTERNALS].referrerPolicy = validateReferrerPolicy(referrerPolicy);
}

/**
* Clone this request
*
Expand All @@ -150,7 +198,9 @@ Object.defineProperties(Request.prototype, {
headers: {enumerable: true},
redirect: {enumerable: true},
clone: {enumerable: true},
signal: {enumerable: true}
signal: {enumerable: true},
referrer: {enumerable: true},
referrerPolicy: {enumerable: true}
});

/**
Expand Down Expand Up @@ -186,6 +236,29 @@ export const getNodeRequestOptions = request => {
headers.set('Content-Length', contentLengthValue);
}

// 4.1. Main fetch, step 2.6
// > If request's referrer policy is the empty string, then set request's referrer policy to the
// > default referrer policy.
if (request.referrerPolicy === '') {
request.referrerPolicy = DEFAULT_REFERRER_POLICY;
}

// 4.1. Main fetch, step 2.7
// > If request's referrer is not "no-referrer", set request's referrer to the result of invoking
// > determine request's referrer.
if (request.referrer && request.referrer !== 'no-referrer') {
request[INTERNALS].referrer = determineRequestsReferrer(request);
} else {
request[INTERNALS].referrer = 'no-referrer';
}

// 4.5. HTTP-network-or-cache fetch, step 6.9
// > If httpRequest's referrer is a URL, then append `Referer`/httpRequest's referrer, serialized
// > and isomorphic encoded, to httpRequest's header list.
if (request[INTERNALS].referrer instanceof URL) {
headers.set('Referer', request.referrer);
}

// HTTP-network-or-cache fetch step 2.11
if (!headers.has('User-Agent')) {
headers.set('User-Agent', 'node-fetch');
Expand Down

0 comments on commit 2d80b0b

Please sign in to comment.