【问题标题】:HTTP2 push and native ES modules: "entry" module push is ignoredHTTP2 推送和原生 ES 模块:“入口”模块推送被忽略
【发布时间】:2018-03-20 10:25:10
【问题描述】:

我一直在尝试通过 HTTP2 为原生 ES 模块提供服务的方法。几乎所有东西都运行良好(在支持的情况下),但有一个我无法理解的怪癖。

给定 / 文档的请求,我推送直接或间接已知为该文档依赖项的资源。在这种情况下,最终会成为通过推送捎带的三个额外资源:

  • /index.css(依赖于<link href ...>
  • /index.js(通过<script type="module" src ...>
  • /routes.js(间接依赖,由index.js导入)

所有三个资源似乎都从服务器端成功推送。然而,Chrome 对“/index.js”提出了第二个请求,尽管第一个请求是推送的。其他两种资源均未请求;这些推送的响应似乎得到了正确的确认。

起初我认为这是 Chrome 的一个怪癖,只是新功能的一个粗略的边缘。但是当启用模块支持标志时,在 Firefox 中也会出现相同的行为,这让我怀疑这是不是出于某种原因故意的。

从后端对应上述请求的日志记录:

RECEIVED REQUEST: GET /
...PUSHING /index.css
...PUSHING /index.js
...PUSHING /routes.js
RECEIVED REQUEST: GET /index.js
...PUSHING /routes.js

按照@sbordet 的说明进行操作:以下是两个请求的记录(很高兴知道这些内容可以在 Chrome 中进行自省!):

第一个请求 (/)

3067: HTTP2_SESSION
death.tips:443 (DIRECT)
Start Time: 2017-10-09 10:49:24.597

t=304289 [st= 0] +HTTP2_SESSION  [dt=?]
                  --> host = "death.tips:443"
                  --> proxy = "DIRECT"
t=304289 [st= 0]    HTTP2_SESSION_INITIALIZED
                    --> protocol = "h2"
                    --> source_dependency = 3064 (SOCKET)
t=304289 [st= 0]    HTTP2_SESSION_SEND_SETTINGS
                    --> settings = ["[id:1 (SETTINGS_HEADER_TABLE_SIZE) value:65536]","[id:3 (SETTINGS_MAX_CONCURRENT_STREAMS) value:1000]","[id:4 (SETTINGS_INITIAL_WINDOW_SIZE) value:6291456]"]
t=304289 [st= 0]    HTTP2_SESSION_UPDATE_RECV_WINDOW
                    --> delta = 15663105
                    --> window_size = 15728640
t=304289 [st= 0]    HTTP2_SESSION_SEND_WINDOW_UPDATE
                    --> delta = 15663105
                    --> stream_id = 0
t=304289 [st= 0]    HTTP2_SESSION_SEND_HEADERS
                    --> exclusive = true
                    --> fin = true
                    --> has_priority = true
                    --> :method: GET
                        :authority: death.tips
                        :scheme: https
                        :path: /
                        pragma: no-cache
                        cache-control: no-cache
                        upgrade-insecure-requests: 1
                        user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3236.0 Safari/537.36
                        accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
                        accept-encoding: gzip, deflate, br
                        accept-language: en-US,en;q=0.9
                    --> parent_stream_id = 0
                    --> source_dependency = 3060 (HTTP_STREAM_JOB)
                    --> stream_id = 1
                    --> weight = 256
t=304310 [st=21]    HTTP2_SESSION_RECV_SETTINGS
t=304310 [st=21]    HTTP2_SESSION_SEND_SETTINGS_ACK
t=304313 [st=24]    HTTP2_SESSION_RECV_SETTINGS_ACK
t=304336 [st=47]    HTTP2_SESSION_RECV_PUSH_PROMISE
                    --> :scheme: https
                        :authority: death.tips
                        :path: /index.css
                        :method: GET
                    --> id = 1
                    --> promised_stream_id = 2
t=304336 [st=47]    HTTP2_STREAM_SEND_PRIORITY
                    --> exclusive = true
                    --> parent_stream_id = 1
                    --> stream_id = 2
                    --> weight = 110
t=304336 [st=47]    HTTP2_SESSION_RECV_PUSH_PROMISE
                    --> :scheme: https
                        :authority: death.tips
                        :path: /index.js
                        :method: GET
                    --> id = 1
                    --> promised_stream_id = 4
t=304336 [st=47]    HTTP2_STREAM_SEND_PRIORITY
                    --> exclusive = true
                    --> parent_stream_id = 2
                    --> stream_id = 4
                    --> weight = 110
t=304336 [st=47]    HTTP2_SESSION_RECV_PUSH_PROMISE
                    --> :scheme: https
                        :authority: death.tips
                        :path: /routes.js
                        :method: GET
                    --> id = 1
                    --> promised_stream_id = 6
t=304336 [st=47]    HTTP2_STREAM_SEND_PRIORITY
                    --> exclusive = true
                    --> parent_stream_id = 4
                    --> stream_id = 6
                    --> weight = 110
t=304336 [st=47]    HTTP2_SESSION_RECV_HEADERS
                    --> fin = false
                    --> :status: 200
                        cache-control: public, max-age=0
                        content-encoding: deflate
                        content-length: 388
                        content-type: text/html; charset=utf-8
                        date: Mon, 09 Oct 2017 14:49:24 GMT
                        etag: "c3QDLn1lTsAqsErFvMgM3bEsUsY="
                        last-modified: Mon, 09 Oct 2017 14:43:24 GMT
                    --> stream_id = 1
t=304336 [st=47]    HTTP2_SESSION_RECV_HEADERS
                    --> fin = false
                    --> :status: 200
                        cache-control: public, max-age=0
                        content-encoding: deflate
                        content-length: 88
                        content-type: text/css
                        date: Mon, 09 Oct 2017 14:49:24 GMT
                        etag: "/qkigeCvJgEE+0+5YhHLgByhKL0="
                        last-modified: Mon, 09 Oct 2017 14:43:24 GMT
                    --> stream_id = 2
t=304336 [st=47]    HTTP2_SESSION_RECV_HEADERS
                    --> fin = false
                    --> :status: 200
                        cache-control: public, max-age=0
                        content-encoding: deflate
                        content-length: 60
                        content-type: text/javascript
                        date: Mon, 09 Oct 2017 14:49:24 GMT
                        etag: "/+cUWoFWkafsB6vSI5wBuB7v4Tk="
                        last-modified: Mon, 09 Oct 2017 14:43:24 GMT
                    --> stream_id = 4
t=304336 [st=47]    HTTP2_SESSION_RECV_HEADERS
                    --> fin = false
                    --> :status: 200
                        cache-control: public, max-age=0
                        content-encoding: deflate
                        content-length: 64
                        content-type: text/javascript
                        date: Mon, 09 Oct 2017 14:49:24 GMT
                        etag: "2ZM3pEXqn9z1d5tkBr2x5kdHsGk="
                        last-modified: Mon, 09 Oct 2017 14:43:24 GMT
                    --> stream_id = 6
t=304336 [st=47]    HTTP2_SESSION_RECV_DATA
                    --> fin = false
                    --> size = 388
                    --> stream_id = 1
t=304336 [st=47]    HTTP2_SESSION_UPDATE_RECV_WINDOW
                    --> delta = -388
                    --> window_size = 15728252
t=304336 [st=47]    HTTP2_SESSION_RECV_DATA
                    --> fin = true
                    --> size = 0
                    --> stream_id = 1
t=304336 [st=47]    HTTP2_SESSION_RECV_DATA
                    --> fin = false
                    --> size = 88
                    --> stream_id = 2
t=304336 [st=47]    HTTP2_SESSION_UPDATE_RECV_WINDOW
                    --> delta = -88
                    --> window_size = 15728164
t=304336 [st=47]    HTTP2_SESSION_RECV_DATA
                    --> fin = true
                    --> size = 0
                    --> stream_id = 2
t=304336 [st=47]    HTTP2_SESSION_RECV_DATA
                    --> fin = false
                    --> size = 60
                    --> stream_id = 4
t=304336 [st=47]    HTTP2_SESSION_UPDATE_RECV_WINDOW
                    --> delta = -60
                    --> window_size = 15728104
t=304336 [st=47]    HTTP2_SESSION_RECV_DATA
                    --> fin = true
                    --> size = 0
                    --> stream_id = 4
t=304336 [st=47]    HTTP2_SESSION_RECV_DATA
                    --> fin = false
                    --> size = 64
                    --> stream_id = 6
t=304336 [st=47]    HTTP2_SESSION_UPDATE_RECV_WINDOW
                    --> delta = -64
                    --> window_size = 15728040
t=304336 [st=47]    HTTP2_SESSION_RECV_DATA
                    --> fin = true
                    --> size = 0
                    --> stream_id = 6
t=304337 [st=48]    HTTP2_SESSION_UPDATE_RECV_WINDOW
                    --> delta = 388
                    --> window_size = 15728428
t=304342 [st=53]    HTTP2_STREAM_ADOPTED_PUSH_STREAM
                    --> stream_id = 2
                    --> url = "https://death.tips/index.css"
t=304343 [st=54]    HTTP2_SESSION_UPDATE_RECV_WINDOW
                    --> delta = 88
                    --> window_size = 15728516

第二个请求 (/index.js)

3085: HTTP2_SESSION
death.tips:443 (DIRECT)
Start Time: 2017-10-09 10:49:24.694

t=304386 [st= 0] +HTTP2_SESSION  [dt=?]
                  --> host = "death.tips:443"
                  --> proxy = "DIRECT"
t=304386 [st= 0]    HTTP2_SESSION_INITIALIZED
                    --> protocol = "h2"
                    --> source_dependency = 3084 (SOCKET)
t=304386 [st= 0]    HTTP2_SESSION_SEND_SETTINGS
                    --> settings = ["[id:1 (SETTINGS_HEADER_TABLE_SIZE) value:65536]","[id:3 (SETTINGS_MAX_CONCURRENT_STREAMS) value:1000]","[id:4 (SETTINGS_INITIAL_WINDOW_SIZE) value:6291456]"]
t=304386 [st= 0]    HTTP2_SESSION_UPDATE_RECV_WINDOW
                    --> delta = 15663105
                    --> window_size = 15728640
t=304386 [st= 0]    HTTP2_SESSION_SEND_WINDOW_UPDATE
                    --> delta = 15663105
                    --> stream_id = 0
t=304386 [st= 0]    HTTP2_SESSION_SEND_HEADERS
                    --> exclusive = true
                    --> fin = true
                    --> has_priority = true
                    --> :method: GET
                        :authority: death.tips
                        :scheme: https
                        :path: /index.js
                        pragma: no-cache
                        cache-control: no-cache
                        origin: https://death.tips
                        user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3236.0 Safari/537.36
                        accept: */*
                        referer: https://death.tips/
                        accept-encoding: gzip, deflate, br
                        accept-language: en-US,en;q=0.9
                    --> parent_stream_id = 0
                    --> source_dependency = 3080 (HTTP_STREAM_JOB)
                    --> stream_id = 1
                    --> weight = 220
t=304405 [st=19]    HTTP2_SESSION_RECV_SETTINGS
t=304405 [st=19]    HTTP2_SESSION_SEND_SETTINGS_ACK
t=304409 [st=23]    HTTP2_SESSION_RECV_SETTINGS_ACK
t=304409 [st=23]    HTTP2_SESSION_RECV_PUSH_PROMISE
                    --> :scheme: https
                        :authority: death.tips
                        :path: /routes.js
                        :method: GET
                    --> id = 1
                    --> promised_stream_id = 2
t=304409 [st=23]    HTTP2_STREAM_SEND_PRIORITY
                    --> exclusive = true
                    --> parent_stream_id = 1
                    --> stream_id = 2
                    --> weight = 110
t=304409 [st=23]    HTTP2_SESSION_RECV_HEADERS
                    --> fin = false
                    --> :status: 200
                        cache-control: public, max-age=0
                        content-encoding: deflate
                        content-length: 60
                        content-type: text/javascript
                        date: Mon, 09 Oct 2017 14:49:24 GMT
                        etag: "/+cUWoFWkafsB6vSI5wBuB7v4Tk="
                        last-modified: Mon, 09 Oct 2017 14:43:24 GMT
                    --> stream_id = 1
t=304409 [st=23]    HTTP2_SESSION_RECV_HEADERS
                    --> fin = false
                    --> :status: 200
                        cache-control: public, max-age=0
                        content-encoding: deflate
                        content-length: 64
                        content-type: text/javascript
                        date: Mon, 09 Oct 2017 14:49:24 GMT
                        etag: "2ZM3pEXqn9z1d5tkBr2x5kdHsGk="
                        last-modified: Mon, 09 Oct 2017 14:43:24 GMT
                    --> stream_id = 2
t=304409 [st=23]    HTTP2_SESSION_RECV_DATA
                    --> fin = false
                    --> size = 60
                    --> stream_id = 1
t=304409 [st=23]    HTTP2_SESSION_UPDATE_RECV_WINDOW
                    --> delta = -60
                    --> window_size = 15728580
t=304409 [st=23]    HTTP2_SESSION_RECV_DATA
                    --> fin = true
                    --> size = 0
                    --> stream_id = 1
t=304409 [st=23]    HTTP2_SESSION_RECV_DATA
                    --> fin = false
                    --> size = 64
                    --> stream_id = 2
t=304409 [st=23]    HTTP2_SESSION_UPDATE_RECV_WINDOW
                    --> delta = -64
                    --> window_size = 15728516
t=304409 [st=23]    HTTP2_SESSION_RECV_DATA
                    --> fin = true
                    --> size = 0
                    --> stream_id = 2
t=304410 [st=24]    HTTP2_SESSION_UPDATE_RECV_WINDOW
                    --> delta = 60
                    --> window_size = 15728576
t=304412 [st=26]    HTTP2_STREAM_ADOPTED_PUSH_STREAM
                    --> stream_id = 2
                    --> url = "https://death.tips/routes.js"
t=304413 [st=27]    HTTP2_SESSION_UPDATE_RECV_WINDOW
                    --> delta = 64
                    --> window_size = 15728640

【问题讨论】:

  • 如果它是相关的:后端是节点,我使用 userland http2/SPDY 包和新的 node-core http2 模块观察到这种行为。
  • 截图中没有显示index.js被浏览器先推送再请求。推送index.js 可能会出现一些问题,以便 Chrome 将其丢弃。您能否使用 Chrome 重试,然后:转到 chrome://net-internals,然后在左侧面板中选择“HTTP/2”,在中间面板中选择与您的服务器的连接。在右侧面板上,您应该会看到 HTTP/2 流量的详细转储,包括推送。在此处报告以供进一步分析。
  • 谢谢 sbordet,我没有意识到有内置的工具。已添加交流的 Chrome 记录以回答。

标签: http2 es6-modules


【解决方案1】:

浏览器活动如下:

  • 发送请求(流=1)
  • 收到/index.css (stream=2) 的推送承诺
  • 收到/index.js (stream=4) 的推送承诺
  • 收到/routes.js (stream=6) 的推送承诺
  • 接收 stream=1 的标头
  • 接收 stream=2 的标头
  • 接收 stream=4 的标头
  • 接收 stream=6 的标头
  • 接收 stream=1 的数据(388+0 字节)
  • 接收 stream=2 的数据(88+0 字节)
  • 接收流=4(60+0 字节)的数据
  • 接收流=6(64+0 字节)的数据

我的解释是,浏览器在接收到推送资源的整个正文之前,收到了主要请求的整个正文(stream=1)。

我猜是浏览器在内部开始解析HTML,发现它需要/index.js,发现它虽然已经承诺但还没有到达,因此它发出了一个请求。

浏览器可能需要/index.css 比它需要/index.js 晚,并且当它需要CSS 时,它已经作为推送资源到达浏览器,这就解释了为什么使用/index.css 从推送缓存。

如果您可以控制何时将资源写入网络,请尝试在发送 HTML 正文之前发送/index.js 的整个正文。这应该让浏览器知道index.js 在推送缓存中完全可用并从那里使用它,而不是重新请求它。

最后一点,Chrome 中的推送缓存实现在过去几年/几个月中变化很大,所以今天可能发生的事情在未来可能不会成立。

【讨论】:

  • 有道理,但我可以看到推送流请求都在事务结束之前完成。但是,这提供了很多帮助! css 在文档中更早,而不是更晚——但 CSS 是 blocking,而 ES 模块本质上是延迟的,即浏览器会尝试立即加载 CSS,但不会尝试加载 ES 模块直到 document.close() 发生。并且 document.close() 在“基本”请求的主体完成之前不会发生,但我将关闭整个请求与结束该有效负载混为一谈(由于使用 HTTP1 兼容 API)。 [...]
  • [...] 如果我的预感是正确的,无论其他因素如何,会话关闭后都无法使用(“采用”)推送流,我必须寻找一种方法在满足原始“请求”后保持 http2 会话打开。今晚将报告这是否有效。
  • 在浏览器实现中假设 blocking 行为时我会小心。上次我查看时,浏览器实现远远胜过任何幼稚的推理,例如“CSS 在文档中较早”或对document.close() 的引用。浏览器在网络层面做什么,然后在渲染层面做什么是两个非常不同的事情,可能没有太大关系。
  • 当然可以在 HTTP/2 会话关闭之前使用推送流(index.css 案例)。您无法控制 HTTP/2 会话何时关闭,因此您的第二条评论不正确。您想尝试在 HTML 内容之前发送index.js
  • 这是一个很好的观点,回复:网络级别,但我提到了 document.close()——不是字面方法,而是文档的打开/关闭状态——因为它与脚本资源的不寻常交互;这似乎是一个很好的探索途径。然而,它并不富有成果[...]
【解决方案2】:

这真是个谜!

问题是——好吧,我无法很好地解释这一点,但我的浅薄理解是,文档是“使用凭据”请求的,但 <script type="module"> 默认情况下会触发“无凭据”要求。脚本的推送承诺是关联的“带有凭据”,但never the twain shall meet。所以浏览器必须发出一个新请求,因为推送承诺“不算数”。 解决方案:

<script type="module" src="/index.js" crossorigin="use-credentials">

我从来没有想过使用“crossorigin”属性来获取同一站点上的资源,但它确实存在。 Push 被采用了,我的小实验也快了两倍。


这是the whole conversation in #whatwg的成绩单:

[7:35pm] <bathos> I’ve got a question about interactions between module
  loading and HTTP2 that’s had me scratching my head for a few days — is
  that something appropriate to ask about here?
[7:37pm] <jyasskin> bathos: Yes.
[7:39pm] <bathos> Cool. I’ve been experimenting with serving resources using
  HTTP2 push — assemble a dep graph in advance and follow through on
  requests by provisioning their known dependencies as push promises. This
  works great on the whole, but there’s a quirk I’ve observed that seems to
  be related specifically to ES module "entrypoints".
[7:40pm] <bathos> I asked about it on SO, so there’s a bit of detail in the
  question and comments there: https://stackoverflow.com/questions/46642569/http2-push-and-native-es-modules-entry-module-push-is-ignored
[7:40pm] <bathos> The gist though:
[7:41pm] <bathos> Given a request for a document which contains
  <script type="module" src="something">, and an http2 session which
  includes a push promise for "something", the "something" push is never
  adopted. Instead, the browser makes a fresh request for it.
[7:41pm] <jyasskin> Domenic: ^
[7:42pm] <bathos> Dependencies imported _in_ ES are adopted.
[7:42pm] <jyasskin> bathos: I'm not an expert here, but your question
  reminds me of the with-vs-no-credentials problem in
  https://github.com/whatwg/fetch/issues/354.
[7:42pm] <bathos> And if I reference the same module in a different way in
  the root document, e.g. a preload <link>, it is successfully adopted. It’s
  peculiar to type="module"
[7:43pm] <bathos> oh, interesting
[7:43pm] <jyasskin> Apologies if I've just sent you on a wild goose chase.
[7:44pm] <bathos> I have been on a lot for the last two days haha! Since
  HTTP2 is still pretty mysterious to me, it’d been hard to rule out the
  possibility that I’m doing something weird there, though I’m pretty sure
  at this point that I’m not.
[7:52pm] <bathos> jyasskin you genius!
[7:53pm] <jyasskin> s/genius/pattern-matcher/ :)
[7:53pm] <bathos> crossorigin="use-credentials" in the doc actually makes
  the module push promise get adopted
[7:54pm] <bathos> I never would have thought to try "crossorigin" on a file
  on the same host haha

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2021-03-08
    • 2017-10-25
    • 2014-07-31
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-11-01
    相关资源
    最近更新 更多