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

Referrer and Referrer Policy #1057

Merged
merged 4 commits into from Nov 5, 2021
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
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 @@ -115,6 +123,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 @@ -139,6 +148,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