arrow_backward Back to blog

Building Streaming REST APIs with Ruby

Twitter popularized the term “firehose API”, to mean a realtime stream of data sent through a persistent connection. But even if you’re not a realtime service, streaming APIs are great for pushing data from the backend to clients. They reduce resource usage because the server can decide when it’s a good time to send a incremental chunk of data. They can also improve the responsiveness of your user experience. The same HTTP API can be reused to power multiple different apps. For example, you could write your web frontend with a Javascript frameworks like Backbone.js, but reuse the same API to power a native iOS application. Follow the jump to read about how streaming APIs work, and how you can write one with Rack::Stream.

TL;DR

Rack::Stream is rack middleware that lets you write streaming API endpoints that understand HTTP, WebSockets, and EventSource. It comes with a DSL and can be used alongside other rackable web frameworks such as Sinatra and Grape.

What’s Streaming HTTP?

Normally, when an HTTP request is made, the server closes the connection when it’s done processing the request. For streaming HTTP, also known as Comet, the main difference is that the server doesn’t close the connection and can continue sending data to the client at a later time.

normal http

streaming http

To prevent the connection from closing, rack-stream uses Thin’s ‘async.callback’ to defer closing the connection until either the server decides to close the connection, or the client disconnects.

Rack::Stream

Rack::Stream is rack middleware that lets you write streaming HTTP endpoints that can understand multiple protocols. Multiple protocols means that you can write an API endpoint that works with curl, but that same endpoint would also works with WebSockets in the browser. The simplest streaming API you can make is:

If you ran this basic rack app, you could then use curl to stream it’s response:

This isn’t very exciting, but you’ll notice that the Transfer-Encoding for the response is set to chunked. By default, rack-stream will take any downstream application’s response bodies and stream them over in chunks. You can read more about chunked transfer encoding on Wikipedia.

Let’s spice it up a bit and build an actual firehose. This next application will keep sending data to the client until the client disconnects:

The first thing to notice is the Firehose rack endpoint includes Rack::Stream::DSL. This are convenience methods that allow you to access env['rack.stream'], which is injected into env whenever you use Rack::Stream. When a request comes in, the #call method schedules a timer that runs every 0.1 seconds and uses the #chunk method to stream data. If you run curl, you would see:

rack-stream also allows you to register callbacks for manipulating response chunks, and controlling when something is sent with different callbacks. Here’s a more advanced example with callbacks added:

If you ran curl now, you would see:

For details about the callbacks, see the project page.

Up until this point, I’ve only used curl to demonstrate hitting the rack endpoint, but one of the big benefits of rack-stream is that it’ll automatically recognize WebSocket and EventSource requests and stream through those as well. For example, you could write an html file that accesses that same endpoint:

Whether you access the endpoint with curl, ajax, or WebSockets, your backend API logic doesn’t have to change.

For the last example, I’ll show a basic chat application using Grape and Rails. The full runnable source is included in the examples/rails directory.

This example uses redis pubsub to push out messages that are created from #post. Thanks to em-synchrony, requests are not blocked when no messages are being sent. It’s important do the redis subscribe after the connection has been opened. Otherwise, the initial response won’t be sent.

What about socket.io?

socket.io is great because it provides many transport fallbacks to give maximum compatibility with many different browsers, but its pubsub interface is too low level for capturing common app semantics. The application developer doesn’t have nice REST features like HTTP verbs, resource URIs, parameter and response encoding, and request headers.

The goal of rack-stream is to provide clean REST-like semantics when you’re developing, but allow you to swap out different transport protocols. Currently, it supports normal HTTP, WebSockets, and EventSource. But the goal is to support more protocols over time and allow custom protocols. This architecture allows socket.io to become another protocol handler that can be plugged into rack-stream. If you wanted to use Pusher as a protocol, that could also be written as a handler for rack-stream.

Summary

rack-stream aims to be a thin abstraction that lets Ruby developers write streaming APIs with their preferred frameworks. I plan to broaden support and test against common use cases and popular frameworks like Sinatra and Rails. If you have any questions or comments, feel free to submit an issue or leave a comment below!

arrow_backBack

New Project Request