Josh
Josh

Reputation: 18690

How do I opt out of HTTP/2 server push when using fetch?

I am writing a basic app in Javascript that uses the new fetch API. Here is a basic example of the relevant portion of the code:

function foo(url) {
  const options = {};
  options.credentials = 'omit';
  options.method = 'get';
  options.headers = {'Accept': 'text/html'};
  options.mode = 'cors';
  options.cache = 'default';
  options.redirect = 'follow';
  options.referrer = 'no-referrer';
  options.referrerPolicy = 'no-referrer';
  return fetch(url, options);
}

When making a fetch request I occasionally see errors appear in the console that look like the following:

Refused to load the script '<url>' because it violates the following Content Security Policy directive ...

After some reading and learning about HTTP/2, it looks like this message appears because the response is pushing back a preloaded script. Using devtools, I can see the following header in the response:

link:<path-to-script>; rel=preload; as=script

Here is the relevant portion of my Chrome extension's manifest.json file:

{
  "content_security_policy": "script-src 'self'; object-src 'self'"
}

Here is documentation on Chrome's manifest.json format, and how the content security policy is applied to fetches made by the extension: https://developer.chrome.com/extensions/contentSecurityPolicy

I did some testing and was able to determine that this error message happens during fetch, not later when parsing the response text. There is no issue where a script element gets loaded into a live DOM, this all happens at the time of the fetch.

What I was not able to find in my research was how to avoid this behavior. It looks like in the rush to support this great new feature, the people that made HTTP/2 and fetch did not consider the use case where I am not fetching the remote page for the purpose of displaying it or any of its associated resources like css/image/script. I (the app) will not ever later be using any associated resource; only the content of the resource itself.

In my use case, this push (1) is a total waste of resources and (2) is now causing a really annoying and stress-inducing message to sporadically appear in the console.

With that said, here is the question I would love some help with: Is there a way to signal to the browser, using manifest or script, that I have no interest in HTTP/2 push? Is there a header I can set for the fetch request that tells the web server to not respond with push? Is there a CSP setting I can use in my app manifest that somehow triggers a do-not-push-me response?

I've looked at https://w3c.github.io/preload/ section 3.3, it was not much help. I see that I can send headers like Link: </dont/want/to/push/this>; rel=preload; as=script; nopush. The problem is that I do not already know which Link headers will be in the response, and I am not sure if fetch even permits setting Link headers in the initial request. I wonder if I can send some type of request that can see the Link headers in the response but avoids them, then send a followup request that appends all the appropriate nopush headers?

Here is a simple test case to reproduce the issue:

  1. Get a dev version of latest or near latest chrome
  2. Create an extension folder
  3. Create manifest with similar CSP
  4. Load extension as unpacked into chrome
  5. Open up the background page for the extension in devtools
  6. In console type fetch('https://www.yahoo.com').
  7. Examine the resulting error message that appears in the console: Refused to load the script 'https://www.yahoo.com/sy/rq/darla/2-9-20/js/g-r-min.js' because it violates the following Content Security Policy directive: "script-src 'self'".

Additional notes:

Upvotes: 27

Views: 2850

Answers (1)

Alex Griffis
Alex Griffis

Reputation: 728

After following along with your test case I was able to resolve this (example of the) issue in the following way, though I don't know that it applies to all more general cases:

  1. Use chrome.webRequest to intercept responses to the extension's requests.
  2. Use the blocking form of onHeadersRecieved to strip out headers containing rel=preload
  3. Allow the response to proceed with the updated headers.

I have to admit I spent a lot of time trying to figure out why this seemed to work, as I don't think stripping the Link headers should work in all cases. I thought that Server Push would just start pushing files after the request is sent.

As you mentioned in your additional note about SETTINGS_ENABLE_PUSH much of this is in fact baked into chrome and hidden from our view. If you want to dig deeper I found the details at chrome://net-internals/#http2. Perhaps Chrome is killing files sent by Server Push that don't have a corresponding Link header in the initial response.

This solution hinges on chrome.webRequest Docs


The extension's background script:

let trackedUrl;

function foo(url) {
  trackedUrl = url;
  const options = {};
  options.credentials = 'omit';
  options.method = 'get';
  options.headers = { 'Accept': 'text/html' };
  options.mode = 'cors';
  options.cache = 'default';
  options.redirect = 'follow';
  options.referrer = 'no-referrer';
  options.referrerPolicy = 'no-referrer';
  return fetch(url, options)
}

chrome.webRequest.onHeadersReceived.addListener(function (details) {
  let newHeaders;
  if (details.url.indexOf(trackedUrl) > -1) {
    newHeaders = details.responseHeaders.filter(header => {
      return header.value.indexOf('rel=preload') < 0;
    })
  }

  return { responseHeaders: newHeaders };
}, { urls: ['<all_urls>'] }, ['responseHeaders', 'blocking']);

The extension's manifest:

{
  "manifest_version": 2,
  "name": "Example",
  "description": "WebRequest Blocking",
  "version": "1.0",
  "browser_action": {
    "default_icon": "icon.png"
  },
  "background": {
    "scripts": [
      "back.js"
    ]
  },
  "content_security_policy": "script-src 'self'; object-src 'self'",
  "permissions": [
    "<all_urls>",
    "background",
    "webRequest",
    "webRequestBlocking"
  ]
}

Additional Notes:

  • I'm just naively limiting this to the latest request url from the extension, there are webRequest.requestFilters baked into chrome.webRequest you can check out here

  • You'll probably also want to be much more specific about which headers you strip. I feel like stripping all the Links will have some additional effects.

  • This avoids proxys and does not require setting a Link header in the request.

  • This makes for a pretty powerful extension, personally I avoid extensions with permissions like <all_urls>, hopefully you can narrow the scope.

  • I did not test for delays caused by blocking the responses to delete headers.

Upvotes: 6

Related Questions