Repository: node-fetch/node-fetch
Pull request #1057 (2021-01-03 21:41:38)

Referrer and Referrer Policy
**What is the purpose of this pull request?**

- [ ] Documentation update
- [ ] Bug fix
- [x] New feature
- [ ] Other, please explain:

**What changes did you make? (provide an overview)**

This adds support for the `referrer` and `referrerPolicy` options.

**Which issue (if any) does this pull request address?**

#1036 #839 

**Is there anything you'd like reviewers to know?**

Specifications are referenced in-line.

tekwiz: @tinovyatkin You are quite right.  I've updated the TS types
YakovL: Hi, is it just lack of reviews that stops this from getting merged?
YakovL: @jimmywarting thanks!

@bitinn looks like this only awaits your review now, would be really nice to merge this.

As a side note: in Chrome dev tools, when one selects "copy as Node.js fetch", the request is copied with `referrer` and `referrerPolicy` options which doesn't work "as is" but will work once this gets merged.
tekwiz: @jimmywarting I had to resolve some merge conflicts and fix some errors, but this is ready to merge now.  To follow good form, would you mind giving it one more once-over and merge if it looks good?

Patch:
diff --git a/@types/index.d.ts b/@types/index.d.ts
index 9854261..7dbc05e 100644
--- a/@types/index.d.ts
+++ b/@types/index.d.ts
@@ -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);
@@ -104,6 +112,9 @@ declare class BodyMixin {
 	readonly bodyUsed: boolean;
 	readonly size: number;
 
+	/**
+ 	* @deprecated Please use 'response.arrayBuffer()' instead of 'response.buffer()
+ 	*/
 	buffer(): Promise<Buffer>;
 	arrayBuffer(): Promise<ArrayBuffer>;
 	blob(): Promise<Blob>;
@@ -115,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);
@@ -139,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;
 }
 
diff --git a/README.md b/README.md
index 4d0652d..2c1198f 100644
--- a/README.md
+++ b/README.md
@@ -240,8 +240,8 @@ import fetch from 'node-fetch';
 
 class HTTPResponseError extends Error {
 	constructor(response, ...args) {
-		this.response = response;
 		super(`HTTP Error Response: ${response.status} ${response.statusText}`, ...args);
+		this.response = response;
 	}
 }
 
@@ -454,7 +454,7 @@ See [test cases](https://github.com/node-fetch/node-fetch/blob/master/test/) for
 
 Perform an HTTP(S) fetch.
 
-`url` should be an absolute url, such as `https://example.com/`. A path-relative URL (`/file/under/root`) or protocol-relative URL (`//can-be-http-or-https.com/`) will result in a rejected `Promise`.
+`url` should be an absolute URL, such as `https://example.com/`. A path-relative URL (`/file/under/root`) or protocol-relative URL (`//can-be-http-or-https.com/`) will result in a rejected `Promise`.
 
 <a id="fetch-options"></a>
 
@@ -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`
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index 4081cf6..781801b 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -6,8 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ## unreleased
 
-- other: Deprecated/Discourage [form-data](https://www.npmjs.com/package/form-data) and body.buffer() (#1212)
+- other: Deprecated/Discourage [form-data](https://www.npmjs.com/package/form-data) and `body.buffer()` (#1212)
 - fix: Normalize `Body.body` into a `node:stream` (#924)
+- fix: Pass url string to `http.request` for parsing IPv6 urls (#1268)
+- fix: Throw error when constructing Request with urls including basic auth (#1268)
 
 ## v3.0.0
 
diff --git a/package.json b/package.json
index 6252c21..189b181 100644
--- a/package.json
+++ b/package.json
@@ -56,13 +56,13 @@
     "delay": "^5.0.0",
     "form-data": "^4.0.0",
     "formdata-node": "^3.5.4",
-    "mocha": "^8.3.2",
+    "mocha": "^9.1.3",
     "p-timeout": "^5.0.0",
     "tsd": "^0.14.0",
     "xo": "^0.39.1"
   },
   "dependencies": {
-    "data-uri-to-buffer": "^3.0.1",
+    "data-uri-to-buffer": "^4.0.0",
     "fetch-blob": "^3.1.2"
   },
   "tsd": {
diff --git a/src/body.js b/src/body.js
index 82991ef..83357f6 100644
--- a/src/body.js
+++ b/src/body.js
@@ -5,8 +5,8 @@
  * Body interface provides common methods for Request and Response
  */
 
-import Stream, {PassThrough} from 'stream';
-import {types, deprecate} from 'util';
+import Stream, {PassThrough} from 'node:stream';
+import {types, deprecate} from 'node:util';
 
 import Blob from 'fetch-blob';
 
diff --git a/src/headers.js b/src/headers.js
index 694d22c..66ea303 100644
--- a/src/headers.js
+++ b/src/headers.js
@@ -4,8 +4,8 @@
  * Headers class offers convenient helpers
  */
 
-import {types} from 'util';
-import http from 'http';
+import {types} from 'node:util';
+import http from 'node:http';
 
 const validateHeaderName = typeof http.validateHeaderName === 'function' ?
 	http.validateHeaderName :
diff --git a/src/index.js b/src/index.js
index 0c5e917..f8686be 100644
--- a/src/index.js
+++ b/src/index.js
@@ -6,10 +6,10 @@
  * All spec algorithm step numbers are based on https://fetch.spec.whatwg.org/commit-snapshots/ae716822cb3a61843226cd090eefc6589446c1d2/.
  */
 
-import http from 'http';
-import https from 'https';
-import zlib from 'zlib';
-import Stream, {PassThrough, pipeline as pump} from 'stream';
+import http from 'node:http';
+import https from 'node:https';
+import zlib from 'node:zlib';
+import Stream, {PassThrough, pipeline as pump} from 'node:stream';
 import dataUriToBuffer from 'data-uri-to-buffer';
 
 import {writeToStream, clone} from './body.js';
@@ -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};
 
@@ -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
@@ -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();
diff --git a/src/request.js b/src/request.js
index 8336150..6d6272c 100644
--- a/src/request.js
+++ b/src/request.js
@@ -7,11 +7,14 @@
  * All spec algorithm step numbers are based on https://fetch.spec.whatwg.org/commit-snapshots/ae716822cb3a61843226cd090eefc6589446c1d2/.
  */
 
-import {format as formatUrl} from 'url';
+import {format as formatUrl} from 'node:url';
 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');
 
@@ -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
@@ -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() {
@@ -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
 	 *
@@ -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}
 });
 
 /**
@@ -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');
diff --git a/src/response.js b/src/response.js
index af820d1..eaba9a9 100644
--- a/src/response.js
+++ b/src/response.js
@@ -95,7 +95,8 @@ export default class Response extends Body {
 			headers: this.headers,
 			ok: this.ok,
 			redirected: this.redirected,
-			size: this.size
+			size: this.size,
+			highWaterMark: this.highWaterMark
 		});
 	}
 
diff --git a/src/utils/form-data.js b/src/utils/form-data.js
index 7b66a8a..ba0c14a 100644
--- a/src/utils/form-data.js
+++ b/src/utils/form-data.js
@@ -1,4 +1,4 @@
-import {randomBytes} from 'crypto';
+import {randomBytes} from 'node:crypto';
 
 import {isBlob} from './is.js';
 
diff --git a/src/utils/referrer.js b/src/utils/referrer.js
new file mode 100644
index 0000000..f9b6817
--- /dev/null
+++ b/src/utils/referrer.js
@@ -0,0 +1,340 @@
+import {isIP} from 'net';
+
+/**
+ * @external URL
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/URL|URL}
+ */
+
+/**
+ * @module utils/referrer
+ * @private
+ */
+
+/**
+ * @see {@link https://w3c.github.io/webappsec-referrer-policy/#strip-url|Referrer Policy §8.4. Strip url for use as a referrer}
+ * @param {string} URL
+ * @param {boolean} [originOnly=false]
+ */
+export function stripURLForUseAsAReferrer(url, originOnly = false) {
+	// 1. If url is null, return no referrer.
+	if (url == null) { // eslint-disable-line no-eq-null, eqeqeq
+		return 'no-referrer';
+	}
+
+	url = new URL(url);
+
+	// 2. If url's scheme is a local scheme, then return no referrer.
+	if (/^(about|blob|data):$/.test(url.protocol)) {
+		return 'no-referrer';
+	}
+
+	// 3. Set url's username to the empty string.
+	url.username = '';
+
+	// 4. Set url's password to null.
+	// Note: `null` appears to be a mistake as this actually results in the password being `"null"`.
+	url.password = '';
+
+	// 5. Set url's fragment to null.
+	// Note: `null` appears to be a mistake as this actually results in the fragment being `"#null"`.
+	url.hash = '';
+
+	// 6. If the origin-only flag is true, then:
+	if (originOnly) {
+		// 6.1. Set url's path to null.
+		// Note: `null` appears to be a mistake as this actually results in the path being `"/null"`.
+		url.pathname = '';
+
+		// 6.2. Set url's query to null.
+		// Note: `null` appears to be a mistake as this actually results in the query being `"?null"`.
+		url.search = '';
+	}
+
+	// 7. Return url.
+	return url;
+}
+
+/**
+ * @see {@link https://w3c.github.io/webappsec-referrer-policy/#enumdef-referrerpolicy|enum ReferrerPolicy}
+ */
+export const ReferrerPolicy = new Set([
+	'',
+	'no-referrer',
+	'no-referrer-when-downgrade',
+	'same-origin',
+	'origin',
+	'strict-origin',
+	'origin-when-cross-origin',
+	'strict-origin-when-cross-origin',
+	'unsafe-url'
+]);
+
+/**
+ * @see {@link https://w3c.github.io/webappsec-referrer-policy/#default-referrer-policy|default referrer policy}
+ */
+export const DEFAULT_REFERRER_POLICY = 'strict-origin-when-cross-origin';
+
+/**
+ * @see {@link https://w3c.github.io/webappsec-referrer-policy/#referrer-policies|Referrer Policy §3. Referrer Policies}
+ * @param {string} referrerPolicy
+ * @returns {string} referrerPolicy
+ */
+export function validateReferrerPolicy(referrerPolicy) {
+	if (!ReferrerPolicy.has(referrerPolicy)) {
+		throw new TypeError(`Invalid referrerPolicy: ${referrerPolicy}`);
+	}
+
+	return referrerPolicy;
+}
+
+/**
+ * @see {@link https://w3c.github.io/webappsec-secure-contexts/#is-origin-trustworthy|Referrer Policy §3.2. Is origin potentially trustworthy?}
+ * @param {external:URL} url
+ * @returns `true`: "Potentially Trustworthy", `false`: "Not Trustworthy"
+ */
+export function isOriginPotentiallyTrustworthy(url) {
+	// 1. If origin is an opaque origin, return "Not Trustworthy".
+	// Not applicable
+
+	// 2. Assert: origin is a tuple origin.
+	// Not for implementations
+
+	// 3. If origin's scheme is either "https" or "wss", return "Potentially Trustworthy".
+	if (/^(http|ws)s:$/.test(url.protocol)) {
+		return true;
+	}
+
+	// 4. If origin's host component matches one of the CIDR notations 127.0.0.0/8 or ::1/128 [RFC4632], return "Potentially Trustworthy".
+	const hostIp = url.host.replace(/(^\[)|(]$)/g, '');
+	const hostIPVersion = isIP(hostIp);
+
+	if (hostIPVersion === 4 && /^127\./.test(hostIp)) {
+		return true;
+	}
+
+	if (hostIPVersion === 6 && /^(((0+:){7})|(::(0+:){0,6}))0*1$/.test(hostIp)) {
+		return true;
+	}
+
+	// 5. If origin's host component is "localhost" or falls within ".localhost", and the user agent conforms to the name resolution rules in [let-localhost-be-localhost], return "Potentially Trustworthy".
+	// We are returning FALSE here because we cannot ensure conformance to
+	// let-localhost-be-loalhost (https://tools.ietf.org/html/draft-west-let-localhost-be-localhost)
+	if (/^(.+\.)*localhost$/.test(url.host)) {
+		return false;
+	}
+
+	// 6. If origin's scheme component is file, return "Potentially Trustworthy".
+	if (url.protocol === 'file:') {
+		return true;
+	}
+
+	// 7. If origin's scheme component is one which the user agent considers to be authenticated, return "Potentially Trustworthy".
+	// Not supported
+
+	// 8. If origin has been configured as a trustworthy origin, return "Potentially Trustworthy".
+	// Not supported
+
+	// 9. Return "Not Trustworthy".
+	return false;
+}
+
+/**
+ * @see {@link https://w3c.github.io/webappsec-secure-contexts/#is-url-trustworthy|Referrer Policy §3.3. Is url potentially trustworthy?}
+ * @param {external:URL} url
+ * @returns `true`: "Potentially Trustworthy", `false`: "Not Trustworthy"
+ */
+export function isUrlPotentiallyTrustworthy(url) {
+	// 1. If url is "about:blank" or "about:srcdoc", return "Potentially Trustworthy".
+	if (/^about:(blank|srcdoc)$/.test(url)) {
+		return true;
+	}
+
+	// 2. If url's scheme is "data", return "Potentially Trustworthy".
+	if (url.protocol === 'data:') {
+		return true;
+	}
+
+	// Note: The origin of blob: and filesystem: URLs is the origin of the context in which they were
+	// created. Therefore, blobs created in a trustworthy origin will themselves be potentially
+	// trustworthy.
+	if (/^(blob|filesystem):$/.test(url.protocol)) {
+		return true;
+	}
+
+	// 3. Return the result of executing §3.2 Is origin potentially trustworthy? on url's origin.
+	return isOriginPotentiallyTrustworthy(url);
+}
+
+/**
+ * Modifies the referrerURL to enforce any extra security policy considerations.
+ * @see {@link https://w3c.github.io/webappsec-referrer-policy/#determine-requests-referrer|Referrer Policy §8.3. Determine request's Referrer}, step 7
+ * @callback module:utils/referrer~referrerURLCallback
+ * @param {external:URL} referrerURL
+ * @returns {external:URL} modified referrerURL
+ */
+
+/**
+ * Modifies the referrerOrigin to enforce any extra security policy considerations.
+ * @see {@link https://w3c.github.io/webappsec-referrer-policy/#determine-requests-referrer|Referrer Policy §8.3. Determine request's Referrer}, step 7
+ * @callback module:utils/referrer~referrerOriginCallback
+ * @param {external:URL} referrerOrigin
+ * @returns {external:URL} modified referrerOrigin
+ */
+
+/**
+ * @see {@link https://w3c.github.io/webappsec-referrer-policy/#determine-requests-referrer|Referrer Policy §8.3. Determine request's Referrer}
+ * @param {Request} request
+ * @param {object} o
+ * @param {module:utils/referrer~referrerURLCallback} o.referrerURLCallback
+ * @param {module:utils/referrer~referrerOriginCallback} o.referrerOriginCallback
+ * @returns {external:URL} Request's referrer
+ */
+export function determineRequestsReferrer(request, {referrerURLCallback, referrerOriginCallback} = {}) {
+	// There are 2 notes in the specification about invalid pre-conditions.  We return null, here, for
+	// these cases:
+	// > Note: If request's referrer is "no-referrer", Fetch will not call into this algorithm.
+	// > Note: If request's referrer policy is the empty string, Fetch will not call into this
+	// > algorithm.
+	if (request.referrer === 'no-referrer' || request.referrerPolicy === '') {
+		return null;
+	}
+
+	// 1. Let policy be request's associated referrer policy.
+	const policy = request.referrerPolicy;
+
+	// 2. Let environment be request's client.
+	// not applicable to node.js
+
+	// 3. Switch on request's referrer:
+	if (request.referrer === 'about:client') {
+		return 'no-referrer';
+	}
+
+	// "a URL": Let referrerSource be request's referrer.
+	const referrerSource = request.referrer;
+
+	// 4. Let request's referrerURL be the result of stripping referrerSource for use as a referrer.
+	let referrerURL = stripURLForUseAsAReferrer(referrerSource);
+
+	// 5. Let referrerOrigin be the result of stripping referrerSource for use as a referrer, with the
+	//    origin-only flag set to true.
+	let referrerOrigin = stripURLForUseAsAReferrer(referrerSource, true);
+
+	// 6. If the result of serializing referrerURL is a string whose length is greater than 4096, set
+	//    referrerURL to referrerOrigin.
+	if (referrerURL.toString().length > 4096) {
+		referrerURL = referrerOrigin;
+	}
+
+	// 7. The user agent MAY alter referrerURL or referrerOrigin at this point to enforce arbitrary
+	//    policy considerations in the interests of minimizing data leakage. For example, the user
+	//    agent could strip the URL down to an origin, modify its host, replace it with an empty
+	//    string, etc.
+	if (referrerURLCallback) {
+		referrerURL = referrerURLCallback(referrerURL);
+	}
+
+	if (referrerOriginCallback) {
+		referrerOrigin = referrerOriginCallback(referrerOrigin);
+	}
+
+	// 8.Execute the statements corresponding to the value of policy:
+	const currentURL = new URL(request.url);
+
+	switch (policy) {
+		case 'no-referrer':
+			return 'no-referrer';
+
+		case 'origin':
+			return referrerOrigin;
+
+		case 'unsafe-url':
+			return referrerURL;
+
+		case 'strict-origin':
+			// 1. If referrerURL is a potentially trustworthy URL and request's current URL is not a
+			//    potentially trustworthy URL, then return no referrer.
+			if (isUrlPotentiallyTrustworthy(referrerURL) && !isUrlPotentiallyTrustworthy(currentURL)) {
+				return 'no-referrer';
+			}
+
+			// 2. Return referrerOrigin.
+			return referrerOrigin.toString();
+
+		case 'strict-origin-when-cross-origin':
+			// 1. If the origin of referrerURL and the origin of request's current URL are the same, then
+			//    return referrerURL.
+			if (referrerURL.origin === currentURL.origin) {
+				return referrerURL;
+			}
+
+			// 2. If referrerURL is a potentially trustworthy URL and request's current URL is not a
+			//    potentially trustworthy URL, then return no referrer.
+			if (isUrlPotentiallyTrustworthy(referrerURL) && !isUrlPotentiallyTrustworthy(currentURL)) {
+				return 'no-referrer';
+			}
+
+			// 3. Return referrerOrigin.
+			return referrerOrigin;
+
+		case 'same-origin':
+			// 1. If the origin of referrerURL and the origin of request's current URL are the same, then
+			//    return referrerURL.
+			if (referrerURL.origin === currentURL.origin) {
+				return referrerURL;
+			}
+
+			// 2. Return no referrer.
+			return 'no-referrer';
+
+		case 'origin-when-cross-origin':
+			// 1. If the origin of referrerURL and the origin of request's current URL are the same, then
+			//    return referrerURL.
+			if (referrerURL.origin === currentURL.origin) {
+				return referrerURL;
+			}
+
+			// Return referrerOrigin.
+			return referrerOrigin;
+
+		case 'no-referrer-when-downgrade':
+			// 1. If referrerURL is a potentially trustworthy URL and request's current URL is not a
+			//    potentially trustworthy URL, then return no referrer.
+			if (isUrlPotentiallyTrustworthy(referrerURL) && !isUrlPotentiallyTrustworthy(currentURL)) {
+				return 'no-referrer';
+			}
+
+			// 2. Return referrerURL.
+			return referrerURL;
+
+		default:
+			throw new TypeError(`Invalid referrerPolicy: ${policy}`);
+	}
+}
+
+/**
+ * @see {@link https://w3c.github.io/webappsec-referrer-policy/#parse-referrer-policy-from-header|Referrer Policy §8.1. Parse a referrer policy from a Referrer-Policy header}
+ * @param {Headers} headers Response headers
+ * @returns {string} policy
+ */
+export function parseReferrerPolicyFromHeader(headers) {
+	// 1. Let policy-tokens be the result of extracting header list values given `Referrer-Policy`
+	//    and response’s header list.
+	const policyTokens = (headers.get('referrer-policy') || '').split(/[,\s]+/);
+
+	// 2. Let policy be the empty string.
+	let policy = '';
+
+	// 3. For each token in policy-tokens, if token is a referrer policy and token is not the empty
+	//    string, then set policy to token.
+	// Note: This algorithm loops over multiple policy values to allow deployment of new policy
+	// values with fallbacks for older user agents, as described in § 11.1 Unknown Policy Values.
+	for (const token of policyTokens) {
+		if (token && ReferrerPolicy.has(token)) {
+			policy = token;
+		}
+	}
+
+	// 4. Return policy.
+	return policy;
+}


Test patch:
diff --git a/test/headers.js b/test/headers.js
index 069a6a1..f57a0b0 100644
--- a/test/headers.js
+++ b/test/headers.js
@@ -1,4 +1,4 @@
-import {format} from 'util';
+import {format} from 'node:util';
 import chai from 'chai';
 import chaiIterator from 'chai-iterator';
 import {Headers} from '../src/index.js';
diff --git a/test/main.js b/test/main.js
index 77d352b..c8ae86e 100644
--- a/test/main.js
+++ b/test/main.js
@@ -1,12 +1,12 @@
 // Test tools
-import zlib from 'zlib';
-import crypto from 'crypto';
-import http from 'http';
-import fs from 'fs';
-import stream from 'stream';
-import path from 'path';
-import {lookup} from 'dns';
-import vm from 'vm';
+import zlib from 'node:zlib';
+import crypto from 'node:crypto';
+import http from 'node:http';
+import fs from 'node:fs';
+import stream from 'node:stream';
+import path from 'node:path';
+import {lookup} from 'node:dns';
+import vm from 'node:vm';
 import chai from 'chai';
 import chaiPromised from 'chai-as-promised';
 import chaiIterator from 'chai-iterator';
@@ -2215,7 +2215,6 @@ describe('node-fetch', () => {
 		function lookupSpy(hostname, options, callback) {
 			called++;
 
-			// eslint-disable-next-line node/prefer-promises/dns
 			return lookup(hostname, options, callback);
 		}
 
@@ -2232,7 +2231,6 @@ describe('node-fetch', () => {
 		function lookupSpy(hostname, options, callback) {
 			families.push(options.family);
 
-			// eslint-disable-next-line node/prefer-promises/dns
 			return lookup(hostname, {}, callback);
 		}
 
diff --git a/test/referrer.js b/test/referrer.js
new file mode 100644
index 0000000..35e6b93
--- /dev/null
+++ b/test/referrer.js
@@ -0,0 +1,552 @@
+import chai from 'chai';
+
+import fetch, {Request, Headers} from '../src/index.js';
+import {
+	DEFAULT_REFERRER_POLICY, ReferrerPolicy, stripURLForUseAsAReferrer, validateReferrerPolicy,
+	isOriginPotentiallyTrustworthy, isUrlPotentiallyTrustworthy, determineRequestsReferrer,
+	parseReferrerPolicyFromHeader
+} from '../src/utils/referrer.js';
+import TestServer from './utils/server.js';
+
+const {expect} = chai;
+
+describe('fetch() with referrer and referrerPolicy', () => {
+	const local = new TestServer();
+	let base;
+
+	before(async () => {
+		await local.start();
+		base = `http://${local.hostname}:${local.port}/`;
+	});
+
+	after(async () => {
+		return local.stop();
+	});
+
+	it('should send request without a referrer by default', () => {
+		return fetch(`${base}inspect`).then(res => res.json()).then(res => {
+			expect(res.headers.referer).to.be.undefined;
+		});
+	});
+
+	it('should send request with a referrer', () => {
+		return fetch(`${base}inspect`, {
+			referrer: base,
+			referrerPolicy: 'unsafe-url'
+		}).then(res => res.json()).then(res => {
+			expect(res.headers.referer).to.equal(base);
+		});
+	});
+
+	it('should send request with referrerPolicy strict-origin-when-cross-origin by default', () => {
+		return Promise.all([
+			fetch(`${base}inspect`, {
+				referrer: base
+			}).then(res => res.json()).then(res => {
+				expect(res.headers.referer).to.equal(base);
+			}),
+			fetch(`${base}inspect`, {
+				referrer: 'https://example.com'
+			}).then(res => res.json()).then(res => {
+				expect(res.headers.referer).to.be.undefined;
+			})
+		]);
+	});
+
+	it('should send request with a referrer and respect redirected referrer-policy', () => {
+		return Promise.all([
+			fetch(`${base}redirect/referrer-policy`, {
+				referrer: base
+			}).then(res => res.json()).then(res => {
+				expect(res.headers.referer).to.equal(base);
+			}),
+			fetch(`${base}redirect/referrer-policy`, {
+				referrer: 'https://example.com'
+			}).then(res => res.json()).then(res => {
+				expect(res.headers.referer).to.be.undefined;
+			}),
+			fetch(`${base}redirect/referrer-policy`, {
+				referrer: 'https://example.com',
+				referrerPolicy: 'unsafe-url'
+			}).then(res => res.json()).then(res => {
+				expect(res.headers.referer).to.equal('https://example.com/');
+			}),
+			fetch(`${base}redirect/referrer-policy/same-origin`, {
+				referrer: 'https://example.com',
+				referrerPolicy: 'unsafe-url'
+			}).then(res => res.json()).then(res => {
+				expect(res.headers.referer).to.undefined;
+			})
+		]);
+	});
+});
+
+describe('Request constructor', () => {
+	describe('referrer', () => {
+		it('should leave referrer undefined by default', () => {
+			const req = new Request('http://example.com');
+			expect(req.referrer).to.be.undefined;
+		});
+
+		it('should accept empty string referrer as no-referrer', () => {
+			const referrer = '';
+			const req = new Request('http://example.com', {referrer});
+			expect(req.referrer).to.equal(referrer);
+		});
+
+		it('should accept about:client referrer as client', () => {
+			const referrer = 'about:client';
+			const req = new Request('http://example.com', {referrer});
+			expect(req.referrer).to.equal(referrer);
+		});
+
+		it('should accept about://client referrer as client', () => {
+			const req = new Request('http://example.com', {referrer: 'about://client'});
+			expect(req.referrer).to.equal('about:client');
+		});
+
+		it('should accept a string URL referrer', () => {
+			const referrer = 'http://example.com/';
+			const req = new Request('http://example.com', {referrer});
+			expect(req.referrer).to.equal(referrer);
+		});
+
+		it('should accept a URL referrer', () => {
+			const referrer = new URL('http://example.com');
+			const req = new Request('http://example.com', {referrer});
+			expect(req.referrer).to.equal(referrer.toString());
+		});
+
+		it('should accept a referrer from input', () => {
+			const referrer = 'http://example.com/';
+			const req = new Request(new Request('http://example.com', {referrer}));
+			expect(req.referrer).to.equal(referrer.toString());
+		});
+
+		it('should throw a TypeError for an invalid URL', () => {
+			expect(() => {
+				const req = new Request('http://example.com', {referrer: 'foobar'});
+				expect.fail(req);
+			}).to.throw(TypeError, 'Invalid URL: foobar');
+		});
+	});
+
+	describe('referrerPolicy', () => {
+		it('should default refererPolicy to empty string', () => {
+			const req = new Request('http://example.com');
+			expect(req.referrerPolicy).to.equal('');
+		});
+
+		it('should accept refererPolicy', () => {
+			const referrerPolicy = 'unsafe-url';
+			const req = new Request('http://example.com', {referrerPolicy});
+			expect(req.referrerPolicy).to.equal(referrerPolicy);
+		});
+
+		it('should accept referrerPolicy from input', () => {
+			const referrerPolicy = 'unsafe-url';
+			const req = new Request(new Request('http://example.com', {referrerPolicy}));
+			expect(req.referrerPolicy).to.equal(referrerPolicy);
+		});
+
+		it('should throw a TypeError for an invalid referrerPolicy', () => {
+			expect(() => {
+				const req = new Request('http://example.com', {referrerPolicy: 'foobar'});
+				expect.fail(req);
+			}).to.throw(TypeError, 'Invalid referrerPolicy: foobar');
+		});
+	});
+});
+
+describe('utils/referrer', () => {
+	it('default policy should be strict-origin-when-cross-origin', () => {
+		expect(DEFAULT_REFERRER_POLICY).to.equal('strict-origin-when-cross-origin');
+	});
+
+	describe('stripURLForUseAsAReferrer', () => {
+		it('should return no-referrer for null/undefined URL', () => {
+			expect(stripURLForUseAsAReferrer(undefined)).to.equal('no-referrer');
+			expect(stripURLForUseAsAReferrer(null)).to.equal('no-referrer');
+		});
+
+		it('should return no-referrer for about:, blob:, and data: URLs', () => {
+			expect(stripURLForUseAsAReferrer('about:client')).to.equal('no-referrer');
+			expect(stripURLForUseAsAReferrer('blob:theblog')).to.equal('no-referrer');
+			expect(stripURLForUseAsAReferrer('data:,thedata')).to.equal('no-referrer');
+		});
+
+		it('should strip the username, password, and hash', () => {
+			const urlStr = 'http://foo:bar@example.com/foo?q=search#theanchor';
+			expect(stripURLForUseAsAReferrer(urlStr).toString())
+				.to.equal('http://example.com/foo?q=search');
+		});
+
+		it('should strip the pathname and query when origin-only', () => {
+			const urlStr = 'http://foo:bar@example.com/foo?q=search#theanchor';
+			expect(stripURLForUseAsAReferrer(urlStr, true).toString())
+				.to.equal('http://example.com/');
+		});
+	});
+
+	describe('validateReferrerPolicy', () => {
+		it('should return the referrer policy', () => {
+			for (const referrerPolicy of ReferrerPolicy) {
+				expect(validateReferrerPolicy(referrerPolicy)).to.equal(referrerPolicy);
+			}
+		});
+
+		it('should throw a TypeError for invalid referrer policies', () => {
+			expect(validateReferrerPolicy.bind(null, undefined))
+				.to.throw(TypeError, 'Invalid referrerPolicy: undefined');
+			expect(validateReferrerPolicy.bind(null, null))
+				.to.throw(TypeError, 'Invalid referrerPolicy: null');
+			expect(validateReferrerPolicy.bind(null, false))
+				.to.throw(TypeError, 'Invalid referrerPolicy: false');
+			expect(validateReferrerPolicy.bind(null, 0))
+				.to.throw(TypeError, 'Invalid referrerPolicy: 0');
+			expect(validateReferrerPolicy.bind(null, 'always'))
+				.to.throw(TypeError, 'Invalid referrerPolicy: always');
+		});
+	});
+
+	const testIsOriginPotentiallyTrustworthyStatements = func => {
+		it('should be potentially trustworthy for HTTPS and WSS URLs', () => {
+			expect(func(new URL('https://example.com'))).to.be.true;
+			expect(func(new URL('wss://example.com'))).to.be.true;
+		});
+
+		it('should be potentially trustworthy for loopback IP address URLs', () => {
+			expect(func(new URL('http://127.0.0.1'))).to.be.true;
+			expect(func(new URL('http://127.1.2.3'))).to.be.true;
+			expect(func(new URL('ws://[::1]'))).to.be.true;
+		});
+
+		it('should not be potentially trustworthy for "localhost" URLs', () => {
+			expect(func(new URL('http://localhost'))).to.be.false;
+		});
+
+		it('should be potentially trustworthy for file: URLs', () => {
+			expect(func(new URL('file://foo/bar'))).to.be.true;
+		});
+
+		it('should not be potentially trustworthy for all other origins', () => {
+			expect(func(new URL('http://example.com'))).to.be.false;
+			expect(func(new URL('ws://example.com'))).to.be.false;
+		});
+	};
+
+	describe('isOriginPotentiallyTrustworthy', () => {
+		testIsOriginPotentiallyTrustworthyStatements(isOriginPotentiallyTrustworthy);
+	});
+
+	describe('isUrlPotentiallyTrustworthy', () => {
+		it('should be potentially trustworthy for about:blank and about:srcdoc', () => {
+			expect(isUrlPotentiallyTrustworthy(new URL('about:blank'))).to.be.true;
+			expect(isUrlPotentiallyTrustworthy(new URL('about:srcdoc'))).to.be.true;
+		});
+
+		it('should be potentially trustworthy for data: URLs', () => {
+			expect(isUrlPotentiallyTrustworthy(new URL('data:,thedata'))).to.be.true;
+		});
+
+		it('should be potentially trustworthy for blob: and filesystem: URLs', () => {
+			expect(isUrlPotentiallyTrustworthy(new URL('blob:theblob'))).to.be.true;
+			expect(isUrlPotentiallyTrustworthy(new URL('filesystem:thefilesystem'))).to.be.true;
+		});
+
+		testIsOriginPotentiallyTrustworthyStatements(isUrlPotentiallyTrustworthy);
+	});
+
+	describe('determineRequestsReferrer', () => {
+		it('should return null for no-referrer or empty referrerPolicy', () => {
+			expect(determineRequestsReferrer({referrer: 'no-referrer'})).to.be.null;
+			expect(determineRequestsReferrer({referrerPolicy: ''})).to.be.null;
+		});
+
+		it('should return no-referrer for about:client', () => {
+			expect(determineRequestsReferrer({
+				referrer: 'about:client',
+				referrerPolicy: DEFAULT_REFERRER_POLICY
+			})).to.equal('no-referrer');
+		});
+
+		it('should return just the origin for URLs over 4096 characters', () => {
+			expect(determineRequestsReferrer({
+				url: 'http://foo:bar@example.com/foo?q=search#theanchor',
+				referrer: `http://example.com/${'0'.repeat(4096)}`,
+				referrerPolicy: DEFAULT_REFERRER_POLICY
+			}).toString()).to.equal('http://example.com/');
+		});
+
+		it('should alter the referrer URL by callback', () => {
+			expect(determineRequestsReferrer({
+				url: 'http://foo:bar@example.com/foo?q=search#theanchor',
+				referrer: 'http://foo:bar@example.com/foo?q=search#theanchor',
+				referrerPolicy: 'unsafe-url'
+			}, {
+				referrerURLCallback: referrerURL => {
+					return new URL(referrerURL.toString().replace(/^http:/, 'myprotocol:'));
+				}
+			}).toString()).to.equal('myprotocol://example.com/foo?q=search');
+		});
+
+		it('should alter the referrer origin by callback', () => {
+			expect(determineRequestsReferrer({
+				url: 'http://foo:bar@example.com/foo?q=search#theanchor',
+				referrer: 'http://foo:bar@example.com/foo?q=search#theanchor',
+				referrerPolicy: 'origin'
+			}, {
+				referrerOriginCallback: referrerOrigin => {
+					return new URL(referrerOrigin.toString().replace(/^http:/, 'myprotocol:'));
+				}
+			}).toString()).to.equal('myprotocol://example.com/');
+		});
+
+		it('should throw a TypeError for an invalid policy', () => {
+			expect(() => {
+				determineRequestsReferrer({
+					url: 'http://foo:bar@example.com/foo?q=search#theanchor',
+					referrer: 'http://foo:bar@example.com/foo?q=search#theanchor',
+					referrerPolicy: 'always'
+				});
+			}).to.throw(TypeError, 'Invalid referrerPolicy: always');
+		});
+
+		const referrerPolicyTestLabel = ({currentURLTrust, referrerURLTrust, sameOrigin}) => {
+			if (currentURLTrust === null && referrerURLTrust === null && sameOrigin === null) {
+				return 'Always';
+			}
+
+			const result = [];
+
+			if (currentURLTrust !== null) {
+				result.push(`Current URL is ${currentURLTrust ? '' : 'not '}potentially trustworthy`);
+			}
+
+			if (referrerURLTrust !== null) {
+				result.push(`Referrer URL is ${referrerURLTrust ? '' : 'not '}potentially trustworthy`);
+			}
+
+			if (sameOrigin !== null) {
+				result.push(`Current URL & Referrer URL do ${sameOrigin ? '' : 'not '}have same origin`);
+			}
+
+			return result.join(', ');
+		};
+
+		const referrerPolicyTests = (referrerPolicy, matrix) => {
+			describe(`Referrer policy: ${referrerPolicy}`, () => {
+				for (const {currentURLTrust, referrerURLTrust, sameOrigin, result} of matrix) {
+					describe(referrerPolicyTestLabel({currentURLTrust, referrerURLTrust, sameOrigin}), () => {
+						const requests = [];
+
+						if (sameOrigin === true || sameOrigin === null) {
+							requests.push({
+								referrerPolicy,
+								url: 'http://foo:bar@example.com/foo?q=search#theanchor',
+								referrer: 'http://foo:bar@example.com/foo?q=search#theanchor'
+							});
+						}
+
+						if (sameOrigin === false || sameOrigin === null) {
+							requests.push({
+								referrerPolicy,
+								url: 'http://foo:bar@example2.com/foo?q=search#theanchor',
+								referrer: 'http://foo:bar@example.com/foo?q=search#theanchor'
+							});
+						}
+
+						let requestsLength = requests.length;
+						switch (currentURLTrust) {
+							case null:
+								for (let i = 0; i < requestsLength; i++) {
+									const req = requests[i];
+									requests.push({...req, url: req.url.replace(/^http:/, 'https:')});
+								}
+
+								break;
+
+							case true:
+								for (let i = 0; i < requestsLength; i++) {
+									const req = requests[i];
+									req.url = req.url.replace(/^http:/, 'https:');
+								}
+
+								break;
+
+							case false:
+								// nothing to do, default is not potentially trustworthy
+								break;
+
+							default:
+								throw new TypeError(`Invalid currentURLTrust condition: ${currentURLTrust}`);
+						}
+
+						requestsLength = requests.length;
+						switch (referrerURLTrust) {
+							case null:
+								for (let i = 0; i < requestsLength; i++) {
+									const req = requests[i];
+
+									if (sameOrigin) {
+										if (req.url.startsWith('https:')) {
+											requests.splice(i, 1);
+										} else {
+											continue;
+										}
+									}
+
+									requests.push({...req, referrer: req.referrer.replace(/^http:/, 'https:')});
+								}
+
+								break;
+
+							case true:
+								for (let i = 0; i < requestsLength; i++) {
+									const req = requests[i];
+									req.referrer = req.referrer.replace(/^http:/, 'https:');
+								}
+
+								break;
+
+							case false:
+								// nothing to do, default is not potentially trustworthy
+								break;
+
+							default:
+								throw new TypeError(`Invalid referrerURLTrust condition: ${referrerURLTrust}`);
+						}
+
+						it('should have tests', () => {
+							expect(requests).to.not.be.empty;
+						});
+
+						for (const req of requests) {
+							it(`should return ${result} for url: ${req.url}, referrer: ${req.referrer}`, () => {
+								if (result === 'no-referrer') {
+									return expect(determineRequestsReferrer(req).toString())
+										.to.equal('no-referrer');
+								}
+
+								if (result === 'referrer-origin') {
+									const referrerOrigih = stripURLForUseAsAReferrer(req.referrer, true);
+									return expect(determineRequestsReferrer(req).toString())
+										.to.equal(referrerOrigih.toString());
+								}
+
+								if (result === 'referrer-url') {
+									const referrerURL = stripURLForUseAsAReferrer(req.referrer);
+									return expect(determineRequestsReferrer(req).toString())
+										.to.equal(referrerURL.toString());
+								}
+
+								throw new TypeError(`Invalid result: ${result}`);
+							});
+						}
+					});
+				}
+			});
+		};
+
+		// 3.1 no-referrer
+		referrerPolicyTests('no-referrer', [
+			{currentURLTrust: null, referrerURLTrust: null, sameOrigin: null, result: 'no-referrer'}
+		]);
+
+		// 3.2 no-referrer-when-downgrade
+		referrerPolicyTests('no-referrer-when-downgrade', [
+			{currentURLTrust: false, referrerURLTrust: true, sameOrigin: null, result: 'no-referrer'},
+			{currentURLTrust: null, referrerURLTrust: false, sameOrigin: null, result: 'referrer-url'},
+			{currentURLTrust: true, referrerURLTrust: true, sameOrigin: null, result: 'referrer-url'}
+		]);
+
+		// 3.3 same-origin
+		referrerPolicyTests('same-origin', [
+			{currentURLTrust: null, referrerURLTrust: null, sameOrigin: false, result: 'no-referrer'},
+			{currentURLTrust: null, referrerURLTrust: null, sameOrigin: true, result: 'referrer-url'}
+		]);
+
+		// 3.4 origin
+		referrerPolicyTests('origin', [
+			{currentURLTrust: null, referrerURLTrust: null, sameOrigin: null, result: 'referrer-origin'}
+		]);
+
+		// 3.5 strict-origin
+		referrerPolicyTests('strict-origin', [
+			{currentURLTrust: false, referrerURLTrust: true, sameOrigin: null, result: 'no-referrer'},
+			{currentURLTrust: null, referrerURLTrust: false, sameOrigin: null, result: 'referrer-origin'},
+			{currentURLTrust: true, referrerURLTrust: true, sameOrigin: null, result: 'referrer-origin'}
+		]);
+
+		// 3.6 origin-when-cross-origin
+		referrerPolicyTests('origin-when-cross-origin', [
+			{currentURLTrust: null, referrerURLTrust: null, sameOrigin: false, result: 'referrer-origin'},
+			{currentURLTrust: null, referrerURLTrust: null, sameOrigin: true, result: 'referrer-url'}
+		]);
+
+		// 3.7 strict-origin-when-cross-origin
+		referrerPolicyTests('strict-origin-when-cross-origin', [
+			{currentURLTrust: false, referrerURLTrust: true, sameOrigin: false, result: 'no-referrer'},
+			{currentURLTrust: null, referrerURLTrust: false, sameOrigin: false,
+				result: 'referrer-origin'},
+			{currentURLTrust: true, referrerURLTrust: true, sameOrigin: false, result: 'referrer-origin'},
+			{currentURLTrust: null, referrerURLTrust: null, sameOrigin: true, result: 'referrer-url'}
+		]);
+
+		// 3.8 unsafe-url
+		referrerPolicyTests('unsafe-url', [
+			{currentURLTrust: null, referrerURLTrust: null, sameOrigin: null, result: 'referrer-url'}
+		]);
+	});
+
+	describe('parseReferrerPolicyFromHeader', () => {
+		it('should return an empty string when no referrer policy is found', () => {
+			expect(parseReferrerPolicyFromHeader(new Headers())).to.equal('');
+			expect(parseReferrerPolicyFromHeader(
+				new Headers([['Referrer-Policy', '']])
+			)).to.equal('');
+		});
+
+		it('should return the last valid referrer policy', () => {
+			expect(parseReferrerPolicyFromHeader(
+				new Headers([['Referrer-Policy', 'no-referrer']])
+			)).to.equal('no-referrer');
+			expect(parseReferrerPolicyFromHeader(
+				new Headers([['Referrer-Policy', 'no-referrer unsafe-url']])
+			)).to.equal('unsafe-url');
+			expect(parseReferrerPolicyFromHeader(
+				new Headers([['Referrer-Policy', 'foo no-referrer bar']])
+			)).to.equal('no-referrer');
+			expect(parseReferrerPolicyFromHeader(
+				new Headers([['Referrer-Policy', 'foo no-referrer unsafe-url bar']])
+			)).to.equal('unsafe-url');
+		});
+
+		it('should use all Referrer-Policy headers', () => {
+			expect(parseReferrerPolicyFromHeader(
+				new Headers([
+					['Referrer-Policy', 'no-referrer'],
+					['Referrer-Policy', '']
+				])
+			)).to.equal('no-referrer');
+			expect(parseReferrerPolicyFromHeader(
+				new Headers([
+					['Referrer-Policy', 'no-referrer'],
+					['Referrer-Policy', 'unsafe-url']
+				])
+			)).to.equal('unsafe-url');
+			expect(parseReferrerPolicyFromHeader(
+				new Headers([
+					['Referrer-Policy', 'no-referrer foo'],
+					['Referrer-Policy', 'bar unsafe-url wow']
+				])
+			)).to.equal('unsafe-url');
+			expect(parseReferrerPolicyFromHeader(
+				new Headers([
+					['Referrer-Policy', 'no-referrer unsafe-url'],
+					['Referrer-Policy', 'foo bar']
+				])
+			)).to.equal('unsafe-url');
+		});
+	});
+});
diff --git a/test/request.js b/test/request.js
index 9d14fd1..de4fed1 100644
--- a/test/request.js
+++ b/test/request.js
@@ -1,5 +1,5 @@
-import stream from 'stream';
-import http from 'http';
+import stream from 'node:stream';
+import http from 'node:http';
 
 import AbortController from 'abort-controller';
 import chai from 'chai';
diff --git a/test/response.js b/test/response.js
index 7c3dab5..0a3b62a 100644
--- a/test/response.js
+++ b/test/response.js
@@ -1,5 +1,4 @@
-
-import * as stream from 'stream';
+import * as stream from 'node:stream';
 import chai from 'chai';
 import Blob from 'fetch-blob';
 import {Response} from '../src/index.js';
@@ -122,7 +121,8 @@ describe('Response', () => {
 			},
 			url: base,
 			status: 346,
-			statusText: 'production'
+			statusText: 'production',
+			highWaterMark: 789
 		});
 		const cl = res.clone();
 		expect(cl.headers.get('a')).to.equal('1');
@@ -130,6 +130,7 @@ describe('Response', () => {
 		expect(cl.url).to.equal(base);
 		expect(cl.status).to.equal(346);
 		expect(cl.statusText).to.equal('production');
+		expect(cl.highWaterMark).to.equal(789);
 		expect(cl.ok).to.be.false;
 		// Clone body shouldn't be the same body
 		expect(cl.body).to.not.equal(body);
diff --git a/test/utils/server.js b/test/utils/server.js
index 329a480..2a1e8e9 100644
--- a/test/utils/server.js
+++ b/test/utils/server.js
@@ -301,6 +301,20 @@ export default class TestServer {
 			res.socket.end('\r\n');
 		}
 
+		if (p === '/redirect/referrer-policy') {
+			res.statusCode = 301;
+			res.setHeader('Location', '/inspect');
+			res.setHeader('Referrer-Policy', 'foo unsafe-url bar');
+			res.end();
+		}
+
+		if (p === '/redirect/referrer-policy/same-origin') {
+			res.statusCode = 301;
+			res.setHeader('Location', '/inspect');
+			res.setHeader('Referrer-Policy', 'foo unsafe-url same-origin bar');
+			res.end();
+		}
+
 		if (p === '/redirect/chunked') {
 			res.writeHead(301, {
 				Location: '/inspect',