google-chromenginxchromiumhttp2server-push

HTTP/2 Server Push results in duplicate requests


A response for a document with the following headers enters Nginx:

link: </picture.jpg>; as=image; rel=preload
link: </_next/static/chunks/commons.4e96503c89eea0476d3e.module.js>; as=script; rel=preload
link: </_next/static/runtime/main-3c17cb16bbbc3efc4bb5.module.js>; as=script; rel=preload
link: </_next/static/runtime/webpack-0b10894b69bf5a718e01.module.js>; as=script; rel=preload
link: </_next/static/Q53NXtgLT1rgpqOOsVV6Q/pages/_app.module.js>; as=script; rel=preload
link: </_next/static/Q53NXtgLT1rgpqOOsVV6Q/pages/index.module.js>; as=script; rel=preload

With the help of HTTP/2 Server Push the requests are Pushed to the client but 5 out of the 6 requests download two times (once with the push and once triggered by the document). The Network tab in Chrome Dev Tools looks like this: network graph I've tested if the Type is set properly and it seems alright. What could be the issue?

Consecutive requests (chrome cache enabled) result in a similar way as well: network graph with cache enabled

What could be wrong? I'm pretty sure the request should not duplicate

@edit I tried doing the Server Push without Nginx (talking directly to Node.js backend instead of the backend attaching link headers for Nginx). It works without an issue. The problem pops up when I use Nginx. push via nodejs

Btw. I do know that one should not push all the contents via Server Push, especially images, but I did it just for a clear test. If you look closer it seems that only the scripts get duplicated and the picture downloads only once.


Solution

  • The core of the problem is actually Chromium. This thing only fails in Chromium from what I can see.

    The problem with Nginx is in the implementation of http2_push_preload.

    What Nginx seeks is a header with Link: </resource>; as=type; rel=preload. It reads it and serves the files via push, unfortunately when the browser (I only tested Chrome actually) receives the document with the Link header as well as the Push it conflicts resulting in a significant slowdown and downloads the resources that were seen while parsing the document instead.

    # This results in HTTP/2 Server Push and the requests get duplicated due to the `Link` headers that were passed along
    location / {
        proxy_pass http://localhost:3000;
        http2_push_preload on;
    }
    
    # This results in Resource Hints getting triggered in the browser.
    location / {
        proxy_pass http://localhost:3000;
    }
    
    # This results in a regular HTTP/2 (no push)
    location / {
        proxy_pass http://localhost:3000;
        http2_push_preload on;
        proxy_hide_header link;
    }
    
    # This result in a valid HTTP/2 Server Push (proper)
    location / {
        proxy_pass http://localhost:3000;
        http2_push /commons.4e96503c89eea0476d3e.module.js;
        http2_push /index.module.js;
        http2_push /_app.module.js;
        http2_push /webpack-0b10894b69bf5a718e01.module.js;
        http2_push /main-3c17cb16bbbc3efc4bb5.module.js;
    }
    

    It seems Nginx does not work well with this feature yet...

    If only I could remove the Link headers and use http2_push_preload...

    Anyway I got it to work with the usage of H2O H2O did let me delete the headers while preserving HTTP/2 Server Push

    // h2o.conf
      [...]
      proxy.reverse.url: "http://host.docker.internal:3000/"
      header.unset: "Link"
    

    Works alright with H2O: H2O success I hope Nginx fixes the way http2_push_preload works and allows for more control.

    Along the side, I think that Chromium should deal with the issue anyway instead of downloading 2 times as many bytes.