使用 fetch 时如何选择退出 HTTP/2 服务器推送?

Posted

技术标签:

【中文标题】使用 fetch 时如何选择退出 HTTP/2 服务器推送?【英文标题】:How do I opt out of HTTP/2 server push when using fetch? 【发布时间】:2018-01-03 06:32:18 【问题描述】:

我正在用 javascript 编写一个使用新的 fetch API 的基本应用程序。以下是代码相关部分的基本示例:

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);

发出获取请求时,我偶尔会在控制台中看到如下所示的错误:

拒绝加载脚本“”,因为它违反了以下内容安全策略指令...

在阅读和了解 HTTP/2 之后,似乎会出现此消息,因为响应正在推回预加载的脚本。使用 devtools,我可以在响应中看到以下标题:

链接:;相对=预载; as=脚本

这是我的 Chrome 扩展的 manifest.json 文件的相关部分:


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

以下是有关 Chrome 的 manifest.json 格式的文档,以及内容安全策略如何应用于扩展程序进行的提取:https://developer.chrome.com/extensions/contentSecurityPolicy

我进行了一些测试,并能够确定此错误消息是在获取期间发生的,而不是稍后在解析响应文本时发生的。脚本元素被加载到实时 DOM 中没有问题,这一切都发生在获取时。

我在研究中找不到的是如何避免这种行为。看起来急于支持这个伟大的新功能,制作 HTTP/2 和 fetch 的人没有考虑我没有获取远程页面以显示它或其任何相关资源(如 css)的用例/图像/脚本。我(应用程序)以后不会使用任何相关资源;只有资源本身的内容。

在我的用例中,此推送 (1) 完全浪费资源,并且 (2) 现在导致控制台中偶尔出现一条非常烦人且会引起压力的消息。

话虽如此,我希望得到一些帮助:有没有办法使用清单或脚本向浏览器发出信号,表明我对 HTTP/2 推送不感兴趣?我可以为获取请求设置一个标头,告诉 Web 服务器不响应推送吗?是否有我可以在我的应用清单中使用的 CSP 设置以某种方式触发 do-not-push-me 响应?

我查看了https://w3c.github.io/preload/ 3.3 节,没有太大帮助。我看到我可以发送像Link: </dont/want/to/push/this>; rel=preload; as=script; nopush 这样的标题。问题是我还不知道响应中将包含哪些 Link 标头,并且我不确定 fetch 是否允许在初始请求中设置 Link 标头。我想知道我是否可以发送某种类型的请求,可以在响应中看到 Link 标头但避免它们,然后发送附加所有适当的 nopush 标头的后续请求?

这是一个重现问题的简单测试用例:

    获取最新或接近最新的 chrome 开发版 创建扩展文件夹 创建具有相似 CSP 的清单 将扩展加载为解压到 chrome 中 在 devtools 中打开扩展的后台页面 在控制台中输入 fetch('https://www.yahoo.com')。 检查控制台中出现的错误消息:拒绝加载脚本'https://www.yahoo.com/sy/rq/darla/2-9-20/js/gr-min .js',因为它违反了以下内容安全策略指令:“script-src 'self'”。

补充说明:

我不想使用代理服务器。一个明确的解释为什么这是我唯一的选择是一个可以接受的答案。 我不知道在配置 CSP 时将获取的 url。 参见https://www.rfc-editor.org/rfc/rfc7540#section-6.5.1,其中在相关部分声明“SETTINGS_ENABLE_PUSH (0x2):此设置可用于禁用服务器推送(第 8.2 节)。如果端点收到此参数设置为值,则不得发送 PUSH_PROMISE 帧的 0." 有没有办法从脚本或清单中指定此设置,或者将其烘焙到 Chrome 中?

【问题讨论】:

为什么你不能只删除响应中的违规标头? @dsign 根据developer.mozilla.org/en-US/docs/Web/API/Response/headers,Response.headers 是只读的。另外,我的理解是,这发生在收到响应时,我的脚本没有时间或地点跳到处理的中间并删除有问题的标头。浏览器处理所有这些。我只能在最后获得访问权限。 抱歉不清楚。我的意思是,你为什么不能在服务器端删除有问题的标头? 啊。好吧,我无法控制服务器或任何其他服务器。我正在用 javascript 编写一个应用程序,它将获取请求发送到各种其他服务器。甚至没有预先确定或预先编译的列表来说明与哪些其他服务器联系。 好吧,我现在明白你的痛苦了。你确定你不会反对普通的CORS protections吗? 【参考方案1】:

遵循您的测试用例后,我能够通过以下方式解决此(示例)问题,但我不知道它是否适用于所有更一般的情况:

    使用chrome.webRequest 拦截对扩展程序请求的响应。 使用onHeadersRecieved 的阻塞形式去除包含rel=preload 的标头 允许响应继续更新标头。

我不得不承认我花了很多时间试图弄清楚为什么这似乎有效,因为我不认为剥离链接标题应该在所有情况下都有效。我以为服务器推送会在请求发送后才开始推送文件。

正如您在关于SETTINGS_ENABLE_PUSH 的附加说明中提到的那样,其中大部分内容实际上已被嵌入到 chrome 中并隐藏在我们的视野之外。如果你想深入挖掘,我在chrome://net-internals/#http2 找到了详细信息。也许 Chrome 正在杀死服务器推送发送的文件,这些文件在初始响应中没有相应的 Link 标头。

此解决方案取决于chrome.webRequest Docs


扩展的后台脚本:

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']);

扩展的清单:


  "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"
  ]

补充说明:

我只是天真地将其限制为来自扩展程序的最新请求 url,webRequest.requestFilters 烘焙到 chrome.webRequest 中,您可以查看 here

您可能还希望更具体地了解您要剥离的标头。我觉得剥离所有链接会产生一些额外的影响。

这样可以避免代理,并且不需要在请求中设置链接头。

这是一个非常强大的扩展,我个人避免使用像&lt;all_urls&gt; 这样的权限的扩展,希望你能缩小范围。

我没有测试因阻止响应删除标头而导致的延迟。

【讨论】:

感谢您的大力支持。我遇到了一些问题。 chrome.webRequest 需要权限。所以我尝试添加权限,现在 Chrome 拒绝加载扩展并说 webRequest 不能用于事件页面。我的背景页面是一个活动页面。嗯。 我对事件页面的理解是它们是按需加载的,而 chrome.webRequest 有可能经常触发,以至于在延迟加载页面上没有意义。你能转换成背景页面吗?尽管首选事件页面,但背景页面仍然可以使用 "persistent": true 标志 more info 谢谢,我正在调查这个。现在不能做太多,但如果它最终有效,将标记为已接受。 您是否有机会看到如何轻松地将 url 限制为仅由扩展程序进行的特定提取,而不是所有请求? 当然,这并不难,当您想从以下提取中清除链接标头时,您只需在foo() 中设置trackedUrl = url。对于您不想执行此操作的提取,请设置 trackedUrl = '&lt;&gt;' 或任何其他不会出现在 url 中的内容。结果是永远不会匹配带有无效字符的trackedUrl,因此不会清除链接头。如果您有更多明确的要求,或者如果您有多个同时请求,则有更好的方法来完成所有这些。这个微型 sn-p 的性能开销几乎为零。

以上是关于使用 fetch 时如何选择退出 HTTP/2 服务器推送?的主要内容,如果未能解决你的问题,请参考以下文章

PHP - 选择然后插入时是不是需要 fetch_array

大家为啥会选择优客服的客服系统啊?

选择另一个 UITextView 时,如何阻止 UITextView 退出第一响应者/关闭键盘?

使用 fetch 时如何从 MVC 控制器返回错误

使用 fetch、cors 时如何从 json API 获取属性?

如何使用 fetch() 获得与 fetchAll() 相同的结果?