Streaming responses using ring
This response is too big to wait until it is all generated to send back. The response time for the client is bad. Can we start sending some it earlier?
I needed to send a large list of files to a browser and have it show a directory tree. The response time of trying to generate the entire list was simply too slow. The user would click in their browser and wait.. and wait.. It was not a good user experience. Luckily, I could use streaming responses to start sending data as soon as a small portion was ready.
Ring streaming at the lowest level,
The lowest level way is to send a ring response with a
:body of type
InputStream is designed to have data read from it, which makes putting data in require a bit of book keeping. However, ring provides
ring.util.io/piped-input-stream which handles this book keeping and provides a
OutputStream for your use. An
OutputStream is designed to have input placed into it, and works with functions like
clojure.core/spit. It is one of the lower level io abstractions in java, and works at the byte level.
However, many libraries will want something that understands more than bytes. For example, clojure.data.xml’s
clojure.data.xml/emit and cheshire’s
chesire.core/generate-stream both expect a
Writer is able to work at the
String level. Clojure provides a function
clojure.java.io/writer to convert an
OutputStream into a
writer we can start putting together a streaming response. Imagine we hava a function
directory-list which will return a xml response, such as from
clojure.data.xml/parse. We’d prefer not to have the entire parse structure and a string for the response in memory due to space and time constraint.
Attempt 1: Sending a large XML response
This looks like it should work, but if we test it out at the repl we can see a problem.
When we try reading from the stream it is empty! This happens due to the intermediate
Writer we have created. It will buffer data to make sure to send efficently.
piped-input-stream will close the underlying
OutputStream when it is done with it, but no one currently tells the
Writer it should be done. So anything stored in its buffer gets lost. To fix this, we need to use
java.io.Writer#flush after the emit.
Then when testing it at a repl, we can get the entire response.
Complete XML streaming example
If we start a server as mentioned in the comment and use curl to check it we can see http://localhost:8080/stream begins streaming “quickly”, where as http://localhost:8080 waits until the entire response is generated.