rails: ActionController::Streaming doesn't stream

ActionController::Streaming appears to not actually stream anything on Rails master.

Reproduction script here. Run it as a rack app with puma config.ru.

What I Expect

Viewing this page in the browser should immediately show “Starting…” and then “1”, “2”, “3” etc should appear gradually as the bytes are streamed to the browser.

What Actually Happens

Nothing is displayed until the entire response has completed rendering.

This can be confirmed in Chrome’s net-internals tab. The request is sent at 3507 milliseconds, and even the headers are not received until 9528 milliseconds.

screen shot 2016-02-23 at 2 32 22 pm

screen shot 2016-02-23 at 2 36 48 pm

I’ve tried a lot of different permutations of this. At first I thought maybe it was my webserver - nope, same behavior with Puma, Webrick and Unicorn (even with the fancy config or whatever you’re supposed to use). Then I thought it was because Rails might be rendering the layout, then flushing, then rendering the view template, then flushing, then back to the layout before flushing again - but no combination of sleeps anywhere produced streaming output.

About this issue

  • Original URL
  • State: open
  • Created 8 years ago
  • Reactions: 2
  • Comments: 22 (20 by maintainers)

Most upvoted comments

The idea that HTTP/2 doesn’t support streaming because it doesn’t support transfer-encoding: chunked is wrong. ALL HTTP/2 streams are full-duplex “chunked” which is why “transfer-encoding: chunked” must not be used because it doesn’t make any sense for HTTP/2 which uses binary framing to sent “chunks”.

@jakeNiemiec I saw HTTP/2 (H2) doesn’t support chunking which initially was a bummer. But from some very brief reading it sounds like H1.1 to H2 proxies can convert H1.1 chunks to H2 streams…

What I’m going to try doing is running Rails over standard H1.1 with chunking enabled, then proxy it to H2 and hopefully get the best of both worlds without rewriting anything. CloudFlare as a free H1.1 to H2 proxy option (via NGINX I believe) so I’ll try that first as low barrier to entry 🎉

If someone fancies rewriting Rails chunking to work with H2 streams (or my above idea doesn’t work) that would be awesome! Unfortunately Rack doesn’t support H2 yet so that would also need rewriting 😬

My findings:

Streaming of the layout works. AKA your head content (stylesheets and whatnot) will stream, then the view <%= yield %> always blocks. That’s what the demo app here demonstrates.

It would of course be nice if views and partials streamed bit by bit too, then slow DB calls wouldn’t block all the content. Nonetheless, streaming in assets and maybe your navigation first is still very useful as the browser can parse the JS and CSS early ready for the content (especially with huge modern JS frameworks that take a while to parse on mobile).

Things that break it:

  • include ActionController::Live bafflingly you can’t use Live and streaming templates in the same controller even though they seem to be 2 sides of the same coin.
  • HAML, Hamlit, Faml templates / views. Erubi and Slim do support streaming. However HAML can still be used in partials without blocking streaming, just can’t be used for your layout & views.
  • Nested layouts as per the Rails Guide because putting yield within content_for blocks streaming. Instead have a application_before.html.haml and application_after.html.haml to extract common layout bits into, so you don’t need to nest.
  • content_for (just switch to provide, easy change, nearly same semantics).
  • Unmet yield :foos. AKA if you have a yield :bar at the top of your template but the provide :bar is within your view then it blocks. Try to change your ordering or unnest views.
  • Rack::Deflate middleware. But you probably don’t need it with a CDN.
  • Webpacker dev proxy broke streaming but I submitted a fix Fix is merged.
  • Bullet, corrupts the stream. Fix submitted here.

@tenderlove’s suggestion content = template.render(view, locals, output, &yielder) does get views and partials to output their contents early without blocking, unfortunately in the wrong order — the layout comes out after.


So provided Webpacker gets fixed I plan to get our app streaming the head, that’ll be a really nice little performance gain site wide for almost no work 😄

I’d guess almost nobody has used streaming since 2016 which is a shame. Perhaps because it’s quite invisible when working, and hard to debug when not. It seems like a really great feature though.

I don’t think this can be closed, because the feature doesn’t do what it’s supposed to.

Since the release of Rack 3 standardises the underlying streaming mechanism, and I added a convenience method #<< must be supported by the output interface, in most cases it should be possible to use a streaming body and use the given stream argument to the ERB (or other template engine) for full streaming support.

IIRC, I could only get streaming to work when I had a layout that had multiple yields. It seemed to be the case that one chunk would be sent for every provide block in the template (note that provide names must be unique as described in the docs).

Early hint headers are meant to be sent before response content, otherwise it has little value. I guess if streaming is used the client will parse the received content and detect required resources anyway. Then it’s not necessary for early hints?