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

fetch: send 'sec-purpose' header for prefetch requests #3694

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion lib/web/fetch/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -120,5 +131,6 @@ module.exports = {
corsSafeListedMethodsSet,
safeMethodsSet,
forbiddenMethodsSet,
referrerPolicySet
referrerPolicySet,
requestInitiator
}
29 changes: 18 additions & 11 deletions lib/web/fetch/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Uzlopak marked this conversation as resolved.
Show resolved Hide resolved
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".
Expand All @@ -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.
Expand All @@ -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.
Expand All @@ -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)) {
Expand All @@ -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:
Expand All @@ -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
Expand Down
13 changes: 11 additions & 2 deletions lib/web/fetch/request.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ const {
requestMode,
requestCredentials,
requestCache,
requestDuplex
requestDuplex,
requestInitiator
} = require('./constants')
const { kEnumerableProperty, normalizedMethodRecordsBase, normalizedMethodRecords } = util
const { webidl } = require('./webidl')
Expand Down Expand Up @@ -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 })
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@KhafraDev Is this right???

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is wrong


// 5. Set fallbackMode to "cors".
fallbackMode = 'cors'
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
},
Comment on lines +1063 to +1068
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is wrong. A Request's request (internal state) contains an initiator, not RequestInit

{
key: 'keepalive',
converter: webidl.converters.boolean
Expand Down
41 changes: 41 additions & 0 deletions test/fetch/initator.js
Original file line number Diff line number Diff line change
@@ -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')
})
})
11 changes: 11 additions & 0 deletions types/fetch.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,13 +118,23 @@ type RequestDestination =
| 'worker'
| 'xslt'

type RequestInitiator =
| ''
| 'download'
| 'imageset'
| 'manifest'
| 'prefetch'
| 'prerender'
| 'xslt'

export interface RequestInit {
method?: string
keepalive?: boolean
headers?: HeadersInit
body?: BodyInit | null
redirect?: RequestRedirect
integrity?: string
initiator?: RequestInitiator
signal?: AbortSignal | null
credentials?: RequestCredentials
mode?: RequestMode
Expand Down Expand Up @@ -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
Expand Down
Loading