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

CROSSORIGIN and CREDENTIALS on prefetch requests #435

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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
2 changes: 1 addition & 1 deletion .size-limit.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@
"limit": "2 kB",
"gzip": true
}
]
]
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,4 @@
"size-limit": "^11.1.4",
"uvu": "^0.5.6"
}
}
}
4 changes: 2 additions & 2 deletions src/chunks.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
**/

import throttle from 'throttles';
import {priority, supported} from './prefetch.mjs';
import {viaFetch, supported} from './prefetch.mjs';
import requestIdleCallback from './request-idle-callback.mjs';

// Cache of URLs we've prefetched
Expand Down Expand Up @@ -145,7 +145,7 @@ export function prefetch(url, isPriority) {
// ~> so that we don't repeat broken links
toPrefetch.add(str);

return (isPriority ? priority : supported)(
return (isPriority ? viaFetch : supported)(
new URL(str, location.href).toString(),
);
}),
Expand Down
53 changes: 29 additions & 24 deletions src/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
**/

import throttle from 'throttles';
import {priority, supported} from './prefetch.mjs';
import { supported, viaFetch } from './prefetch.mjs';
import requestIdleCallback from './request-idle-callback.mjs';
import {addSpeculationRules, hasSpecRulesSupport} from './prerender.mjs';
import { addSpeculationRules, hasSpecRulesSupport } from './prerender.mjs';

// Cache of URLs we've prefetched
// Its `size` is compared against `opts.limit` value.
Expand Down Expand Up @@ -72,6 +72,8 @@ function checkConnection(conn) {
* @param {Object} options - Configuration options for quicklink
* @param {Object|Array} [options.el] - DOM element(s) to prefetch in-viewport links of
* @param {Boolean} [options.priority] - Attempt higher priority fetch (low or high)
* @param {Boolean} [options.checkAccessControlAllowOrigin] - Check Access-Control-Allow-Origin response header
* @param {Boolean} [options.checkAccessControlAllowCredentials] - Check the Access-Control-Allow-Credentials response header
* @param {Array} [options.origins] - Allowed origins to prefetch (empty allows all)
* @param {Array|RegExp|Function} [options.ignores] - Custom filter(s) that run after origin checks
* @param {Number} [options.timeout] - Timeout after which prefetching will occur
Expand Down Expand Up @@ -147,16 +149,17 @@ export function listen(options = {}) {
// Do not prefetch if will match/exceed limit and user has not switched to shouldOnlyPrerender mode
if (toPrefetch.size < limit && !shouldOnlyPrerender) {
toAdd(() => {
prefetch(hrefFn ? hrefFn(entry) : entry.href, options.priority)
.then(isDone)
.catch(error => {
isDone();
if (options.onError) options.onError(error);
});
prefetch(hrefFn ? hrefFn(entry) : entry.href, options.priority,
options.checkAccessControlAllowOrigin, options.checkAccessControlAllowCredentials)
.then(isDone)
.catch(error => {
isDone();
if (options.onError) options.onError(error);
});
});
}
}, delay);
// On exit
// On exit
} else {
entry = entry.target;
const index = hrefsInViewport.indexOf(entry.href);
Expand All @@ -172,9 +175,9 @@ export function listen(options = {}) {
timeoutFn(() => {
// Find all links & Connect them to IO if allowed
const elementsToListen = options.el &&
options.el.length &&
options.el.length > 0 &&
options.el[0].nodeName === 'A' ?
options.el.length &&
options.el.length > 0 &&
options.el[0].nodeName === 'A' ?
options.el :
(options.el || document).querySelectorAll('a');

Expand All @@ -201,10 +204,13 @@ export function listen(options = {}) {
/**
* Prefetch a given URL with an optional preferred fetch priority
* @param {String} url - the URL to fetch
* @param {Boolean} [isPriority] - if is "high" priority
* @param {Boolean} isPriority - if is "high" priority
* @param {Boolean} checkAccessControlAllowOrigin - true to set crossorigin="anonymous" for DOM prefetch
* and mode:'cors' for API fetch
* @param {Boolean} checkAccessControlAllowCredentials - true to set credentials:'include' for API fetch
* @return {Object} a Promise
*/
export function prefetch(url, isPriority) {
export function prefetch(url, isPriority, checkAccessControlAllowOrigin, checkAccessControlAllowCredentials) {
const chkConn = checkConnection(navigator.connection);
if (chkConn instanceof Error) {
return Promise.reject(new Error(`Cannot prefetch, ${chkConn.message}`));
Expand All @@ -216,17 +222,16 @@ export function prefetch(url, isPriority) {

// Dev must supply own catch()
return Promise.all(
[].concat(url).map(str => {
if (toPrefetch.has(str)) return [];
[].concat(url).map(str => {
if (toPrefetch.has(str)) return [];

// Add it now, regardless of its success
// ~> so that we don't repeat broken links
toPrefetch.add(str);
// Add it now, regardless of its success
// ~> so that we don't repeat broken links
toPrefetch.add(str);

return (isPriority ? priority : supported)(
new URL(str, location.href).toString(),
);
}),
return (isPriority ? viaFetch : supported)(new URL(str, location.href).toString(),
checkAccessControlAllowOrigin, checkAccessControlAllowCredentials, isPriority);
}),
);
}

Expand All @@ -245,7 +250,7 @@ export function prerender(urls) {
// 1) whether UA supports spec rules.. If not, fallback to prefetch
// Note: Prerendering supports same-site cross origin with opt-in header
if (!hasSpecRulesSupport()) {
prefetch(urls);
prefetch(urls, true, false, false);
return Promise.reject(new Error('This browser does not support the speculation rules API. Falling back to prefetch.'));
}

Expand Down
28 changes: 21 additions & 7 deletions src/prefetch.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
/**
* Checks if a feature on `link` is natively supported.
* Examples of features include `prefetch` and `preload`.
* @param {Object} link Link object.
* @param {Object} link - Link object.
* @return {Boolean} whether the feature is supported
*/
function hasPrefetch(link) {
Expand All @@ -31,13 +31,17 @@ function hasPrefetch(link) {
/**
* Fetches a given URL using `<link rel=prefetch>`
* @param {string} url - the URL to fetch
* @param {Boolean} hasCrossorigin - true to set crossorigin="anonymous"
* @return {Object} a Promise
*/
function viaDOM(url) {
function viaDOM(url, hasCrossorigin) {
return new Promise((resolve, reject, link) => {
link = document.createElement('link');
link.rel = 'prefetch';
link.href = url;
if (hasCrossorigin) {
link.setAttribute('crossorigin', 'anonymous');
}

link.onload = resolve;
link.onerror = reject;
Expand All @@ -49,13 +53,16 @@ function viaDOM(url) {
/**
* Fetches a given URL using XMLHttpRequest
* @param {string} url - the URL to fetch
* @param {Boolean} hasCredentials - true to set withCredentials:true
* @return {Object} a Promise
*/
function viaXHR(url) {
function viaXHR(url, hasCredentials) {
return new Promise((resolve, reject, request) => {
request = new XMLHttpRequest();

request.open('GET', url, request.withCredentials = true);
request.open('GET', url, request.withCredentials = hasCredentials);

request.setRequestHeader('Accept', '*/*');

request.onload = () => {
if (request.status === 200) {
Expand All @@ -74,17 +81,24 @@ function viaXHR(url) {
* Fetches a given URL using the Fetch API. Falls back
* to XMLHttpRequest if the API is not supported.
* @param {string} url - the URL to fetch
* @param {Boolean} hasModeCors - true to set mode:'cors'
* @param {Boolean} hasCredentials - true to set credentials:'include'
* @param {Boolean} isPriority - true to set priority:'high'
* @return {Object} a Promise
*/
export function priority(url) {
export function viaFetch(url, hasModeCors, hasCredentials, isPriority) {
// TODO: Investigate using preload for high-priority
// fetches. May have to sniff file-extension to provide
// valid 'as' values. In the future, we may be able to
// use Priority Hints here.
//
// As of 2018, fetch() is high-priority in Chrome
// and medium-priority in Safari.
return window.fetch ? fetch(url, {credentials: 'include'}) : viaXHR(url);
const options = {headers: {accept: '*/*'}};
if (!hasModeCors) options.mode = 'no-cors';
if (hasCredentials) options.credentials = 'include';
isPriority ? options.priority = 'high' : options.priority = 'low';
return window.fetch ? fetch(url, options) : viaXHR(url, hasCredentials);
}

export const supported = hasPrefetch() ? viaDOM : viaXHR;
export const supported = hasPrefetch() ? viaDOM : viaFetch;
1 change: 1 addition & 0 deletions src/prerender.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
/**
* Add a given set of urls to the speculation rules
* @param {Set} urlsToPrerender - the URLs to add to speculation rules
* @param {String} eagerness - prerender eagerness mode
* @return {Boolean|Object} boolean or Error Object
*/
export function addSpeculationRules(urlsToPrerender) {
Expand Down