diff --git a/lib/web/fetch/constants.js b/lib/web/fetch/constants.js index 1f285e06283..472e3277cfa 100644 --- a/lib/web/fetch/constants.js +++ b/lib/web/fetch/constants.js @@ -97,8 +97,19 @@ const subresource = /** @type {const} */ ([ 'xslt', '' ]) + const subresourceSet = new Set(subresource) +const requestInitiator = /** @type {const} */ ([ + 'download', + 'imageset', + 'manifest', + 'prefetch', + 'prerender', + 'xslt', + '' +]) + module.exports = { subresource, forbiddenMethods, @@ -120,5 +131,6 @@ module.exports = { corsSafeListedMethodsSet, safeMethodsSet, forbiddenMethodsSet, - referrerPolicySet + referrerPolicySet, + requestInitiator } diff --git a/lib/web/fetch/index.js b/lib/web/fetch/index.js index b2c95d94a09..9547a58951f 100644 --- a/lib/web/fetch/index.js +++ b/lib/web/fetch/index.js @@ -1449,14 +1449,21 @@ async function httpNetworkOrCacheFetch ( // 13. Append the Fetch metadata headers for httpRequest. [FETCH-METADATA] appendFetchMetadata(httpRequest) - // 14. If httpRequest’s header list does not contain `User-Agent`, then + // 14. If httpRequest’s initiator is "prefetch", then set a structured + // field value given (`Sec-Purpose`, the token 'prefetch') in httpRequest’s + // header list. + if (httpRequest.initiator === 'prefetch') { + httpRequest.headersList.append('sec-purpose', 'prefetch', true) + } + + // 15. If httpRequest’s header list does not contain `User-Agent`, then // user agents should append `User-Agent`/default `User-Agent` value to // httpRequest’s header list. if (!httpRequest.headersList.contains('user-agent', true)) { httpRequest.headersList.append('user-agent', defaultUserAgent, true) } - // 15. If httpRequest’s cache mode is "default" and httpRequest’s header + // 16. If httpRequest’s cache mode is "default" and httpRequest’s header // list contains `If-Modified-Since`, `If-None-Match`, // `If-Unmodified-Since`, `If-Match`, or `If-Range`, then set // httpRequest’s cache mode to "no-store". @@ -1471,7 +1478,7 @@ async function httpNetworkOrCacheFetch ( httpRequest.cache = 'no-store' } - // 16. If httpRequest’s cache mode is "no-cache", httpRequest’s prevent + // 17. If httpRequest’s cache mode is "no-cache", httpRequest’s prevent // no-cache cache-control header modification flag is unset, and // httpRequest’s header list does not contain `Cache-Control`, then append // `Cache-Control`/`max-age=0` to httpRequest’s header list. @@ -1483,7 +1490,7 @@ async function httpNetworkOrCacheFetch ( httpRequest.headersList.append('cache-control', 'max-age=0', true) } - // 17. If httpRequest’s cache mode is "no-store" or "reload", then: + // 18. If httpRequest’s cache mode is "no-store" or "reload", then: if (httpRequest.cache === 'no-store' || httpRequest.cache === 'reload') { // 1. If httpRequest’s header list does not contain `Pragma`, then append // `Pragma`/`no-cache` to httpRequest’s header list. @@ -1498,13 +1505,13 @@ async function httpNetworkOrCacheFetch ( } } - // 18. If httpRequest’s header list contains `Range`, then append + // 19. If httpRequest’s header list contains `Range`, then append // `Accept-Encoding`/`identity` to httpRequest’s header list. if (httpRequest.headersList.contains('range', true)) { httpRequest.headersList.append('accept-encoding', 'identity', true) } - // 19. Modify httpRequest’s header list per HTTP. Do not append a given + // 20. Modify httpRequest’s header list per HTTP. Do not append a given // header if httpRequest’s header list contains that header’s name. // TODO: https://github.com/whatwg/fetch/issues/1285#issuecomment-896560129 if (!httpRequest.headersList.contains('accept-encoding', true)) { @@ -1517,7 +1524,7 @@ async function httpNetworkOrCacheFetch ( httpRequest.headersList.delete('host', true) - // 20. If includeCredentials is true, then: + // 21. If includeCredentials is true, then: if (includeCredentials) { // 1. If the user agent is not configured to block cookies for httpRequest // (see section 7 of [COOKIES]), then: @@ -1526,20 +1533,20 @@ async function httpNetworkOrCacheFetch ( // TODO: credentials } - // 21. If there’s a proxy-authentication entry, use it as appropriate. + // 22. If there’s a proxy-authentication entry, use it as appropriate. // TODO: proxy-authentication - // 22. Set httpCache to the result of determining the HTTP cache + // 23. Set httpCache to the result of determining the HTTP cache // partition, given httpRequest. // TODO: cache - // 23. If httpCache is null, then set httpRequest’s cache mode to + // 24. If httpCache is null, then set httpRequest’s cache mode to // "no-store". if (httpCache == null) { httpRequest.cache = 'no-store' } - // 24. If httpRequest’s cache mode is neither "no-store" nor "reload", + // 25. If httpRequest’s cache mode is neither "no-store" nor "reload", // then: if (httpRequest.cache !== 'no-store' && httpRequest.cache !== 'reload') { // TODO: cache diff --git a/lib/web/fetch/request.js b/lib/web/fetch/request.js index b6a0d6dfdfb..b60631497bf 100644 --- a/lib/web/fetch/request.js +++ b/lib/web/fetch/request.js @@ -20,7 +20,8 @@ const { requestMode, requestCredentials, requestCache, - requestDuplex + requestDuplex, + requestInitiator } = require('./constants') const { kEnumerableProperty, normalizedMethodRecordsBase, normalizedMethodRecords } = util const { webidl } = require('./webidl') @@ -136,7 +137,7 @@ class Request { } // 4. Set request to a new request whose URL is parsedURL. - request = makeRequest({ urlList: [parsedURL] }) + request = makeRequest({ urlList: [parsedURL], ...init }) // 5. Set fallbackMode to "cors". fallbackMode = 'cors' @@ -215,6 +216,8 @@ class Request { redirect: request.redirect, // integrity metadata request’s integrity metadata. integrity: request.integrity, + // initiator metadata request’s initiator metadata. + initiator: request.initiator, // keepalive request’s keepalive. keepalive: request.keepalive, // reload-navigation flag request’s reload-navigation flag. @@ -1057,6 +1060,12 @@ webidl.converters.RequestInit = webidl.dictionaryConverter([ key: 'integrity', converter: webidl.converters.DOMString }, + { + key: 'initiator', + converter: webidl.converters.DOMString, + // https://fetch.spec.whatwg.org/#concept-request-initiator + allowedValues: requestInitiator + }, { key: 'keepalive', converter: webidl.converters.boolean diff --git a/test/fetch/initator.js b/test/fetch/initator.js new file mode 100644 index 00000000000..0de76ee1c0b --- /dev/null +++ b/test/fetch/initator.js @@ -0,0 +1,41 @@ +'use strict' + +const { test, describe, before, after } = require('node:test') +const assert = require('node:assert') +const events = require('node:events') +const http = require('node:http') +const { fetch, Request } = require('../../') +const { closeServerAsPromise } = require('../utils/node-http') + +describe('initiator', () => { + const server = http.createServer((req, res) => { + res.end(req.headers['sec-purpose']) + }) + + before(async () => { + server.listen(0) + await events.once(server, 'listening') + }) + + after(closeServerAsPromise(server)) + + test('if initiator is not "prefetch" then sec-purpose is not set', async (t) => { + const url = `http://localhost:${server.address().port}` + + const response = await fetch(url, { + initiator: '' + }) + + assert.strictEqual(await response.text(), '') + }) + + test('if initiator is set to prefetch then the sec-purpose header is set to "prefetch"', async (t) => { + const url = `http://localhost:${server.address().port}` + + const response = await fetch(new Request(url, { + initiator: 'prefetch' + })) + + assert.deepStrictEqual(await response.text(), 'prefetch') + }) +}) diff --git a/types/fetch.d.ts b/types/fetch.d.ts index 7c2b1c98ccd..2317e6564be 100644 --- a/types/fetch.d.ts +++ b/types/fetch.d.ts @@ -118,6 +118,15 @@ type RequestDestination = | 'worker' | 'xslt' +type RequestInitiator = + | '' + | 'download' + | 'imageset' + | 'manifest' + | 'prefetch' + | 'prerender' + | 'xslt' + export interface RequestInit { method?: string keepalive?: boolean @@ -125,6 +134,7 @@ export interface RequestInit { body?: BodyInit | null redirect?: RequestRedirect integrity?: string + initiator?: RequestInitiator signal?: AbortSignal | null credentials?: RequestCredentials mode?: RequestMode @@ -160,6 +170,7 @@ export declare class Request extends BodyMixin { readonly destination: RequestDestination readonly headers: Headers readonly integrity: string + readonly initiator: RequestInitiator readonly method: string readonly mode: RequestMode readonly redirect: RequestRedirect