2

Configuration

  • Rails: 4.2.7.1
  • Puma: 3.8.2

--

Transfer-Encoding:  chunked

I've been unable to make this work and unable to find a definitive answer: in the above configuration, I want to stream large amounts of data to the client (in the response) - is this supported?

  • If so, what is my responsibility?
    • Should my controller be emitting hexadecimal chunk sizes, \r\n, 0 etc?
  • It feels like I want but can't find a simple response API like:
    • write, write, write, flush
    • write, write, write, flush
    • close
  • I've read 100 posts about Rack, monkey patching and other insanity
  • I've read about Puma and/or Rack mangling the encoding, possibly gzip/deflating in the wrong order
  • This seems like a simple feature that should be readily available but I am stumped
  • I have created numerous tests, e.g. self.response_body = Enumerator.new and response.stream.write, etc. -- all with similar results (via curl) - Malformed encoding found in chunked-encoding or transfer closed with outstanding read data remaining

Can anyone show me the light?

Publius
  • 53
  • 1
  • 5

2 Answers2

1

The ActionController::Live API offers exactly what you describe: response.stream.write and response.stream.close. (write flushes a chunk automatically; you'll need to do your own buffering if that doesn't work for you.)

As long as you include ActionController::Live (and note that affects the behaviour of the entire controller, not just one action), you should then be able to write a streaming response with no further effort: you need not, and should not, set any headers etc related to chunking.

Additional reference: http://tenderlovemaking.com/2012/07/30/is-it-live.html

matthewd
  • 4,332
  • 15
  • 21
  • Not exactly - the API you cite sets the `Content-Type: text/event-stream` header which not all browsers support. (AFAIK) Also, I don't t think this is a scalable solution due to threading implementation. – Publius Aug 07 '18 at 14:00
  • The _example_ uses that content type; you can set yours to whatever you like. – matthewd Aug 07 '18 at 14:13
  • I think you're confused - the `Transfer-Encoding` not the `Content-Type` header is what's relevant to this discussion. Presumably you know how `Transfer-Encoding: chunked` works and how the client is to process it. The `Content-Type: text/event-stream` produces server side events and not all clients (browsers) support this. All browsers, on the other hand, support `Transfer-Encoding: chunked`. – Publius Aug 07 '18 at 18:20
  • def show response.headers["Transfer-Encoding"] = "chunked" 100.times { response.stream.write "hello world\n" } ensure response.stream.close end Result: HTTP/1.1 200 OK Transfer-Encoding: chunked Cache-Control: no-cache Content-Type: text/html; charset=utf-8 Connection: close Server: thin curl: (56) Illegal or missing hexadecimal sequence in chunked-encoding @matthewd – Publius Aug 07 '18 at 18:34
  • _you need not, **and should not**, set any headers etc related to chunking_. (Also, thin isn't puma.) – matthewd Aug 08 '18 at 00:43
  • OK - I think we're going around in circles then as my original question still stands - how to employ `Transfer-Encoding: chunked`. `Content-Type: text/event-stream` (i.e., server sent events) although interesting is not what I'm asking about. I do appreciate your comments nonetheless. @matthewd – Publius Aug 08 '18 at 15:16
  • Follow the example in the ActionController::Live doc. Set the content type to whatever you like, but change nothing else. The chunking will be managed by Puma. By trying to set the header yourself you are disabling the built-in functionality. – matthewd Aug 08 '18 at 15:25
  • you are 100% correct. I did not understand what was happening in my development environment - I did not realize that Thin was running instead of Puma. In fact, I didn't even know "Thin" was a thing. – Publius Aug 08 '18 at 19:31
1

Important addendum to @matthewd 's wonderful answer:

The Rack specification supports a streaming response through the use of the each method on the response object or through the use of hijack.

@matthewd is right when stating that:

The ActionController::Live API offers exactly what you describe...

However, the implementation either hijacks the socket or uses the Rack specification's "hack" with the each method.

The best case scenario is that the implementation hijacks the socket and runs it on a new thread (which is what it should normally do, AFAIK).

However, this could result in quite a lot of threads and could become performance heavy - threads cost in memory space for stack data (1Mb-2Mb per thread / client) and context switches become more expensive as more threads are created.

In the worst case scenario, a slow each loop will block a server's thread, crippling the server and eventually resulting in a DoS situation.

The correct answer should be NOT to stream data over a single HTTP request - use a native* WebSockets, SSE or AJAX solution instead.

Another semi-correct method would save all the data to a temporary file and sending the file using a server that supports static file streaming outside the Ruby layer (such as iodine) or a proxy (such as nginx).

* Native: A native WebSocket / SSE solution follows this Rack proposal and allows the server to handle the network layer rather than running another thread / IO reactor. See this blog post for more details.

Myst
  • 18,516
  • 2
  • 45
  • 67