Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Reading Ring request body when already read

Tags:

clojure

ring

My question is, how can I idiomatically read the body of a Ring request if it has already been read?

Here's the background. I'm writing an error handler for a Ring app. When an error occurs, I want to log the error, including all relevant information that I might need to reproduce and fix the error. One important piece of information is the body of the request. However, the statefulness of the :body value (because it is a type of java.io.InputStream object) causes problems.

Specifically, what happens is that some middleware (the ring.middleware.json/wrap-json-body middleware in my case) does a slurp on the body InputStream object, which changes the internal state of the object such that future calls to slurp return an empty string. Thus, the [content of the] body is effectively lost from the request map.

The only solution I can think of is to preemptively copy the body InputStream object before the body can be read, just in case I might need it later. I don't like this approach because it seems clumsy to do some work on every request just in case there might be an error later. Is there a better approach?

like image 738
Jeff Terrell Ph.D. Avatar asked Dec 12 '13 20:12

Jeff Terrell Ph.D.


2 Answers

I have a lib that sucks up the body, replaces it with a stream with identical contents, and stores the original so that it can be deflated later.

groundhog

This is not adequate for indefinitely open streams, and is a bad idea if the body is the upload of some large object. But it helps for testing, and recreating error conditions as a part of the debugging process.

If all you need is a duplicate of the stream, you can use the tee-stream function from groundhog as the basis for your own middleware.

like image 160
noisesmith Avatar answered Oct 22 '22 05:10

noisesmith


I adopted @noisesmith's basic approach with a few modifications, as shown below. Each of these functions can be used as Ring middleware.

(defn with-request-copy
  "Transparently store a copy of the request in the given atom.
  Blocks until the entire body is read from the request.  The request
  stored in the atom (which is also the request passed to the handler)
  will have a body that is a fresh (and resettable) ByteArrayInputStream
  object."
  [handler atom]
  (fn [{orig-body :body :as request}]
    (let [{body :stream} (groundhog/tee-stream orig-body)
          request-copy (assoc request :body body)]
      (reset! atom request-copy)
      (handler request-copy))))

(defn wrap-error-page
  "In the event of an exception, do something with the exception
  (e.g. report it using an exception handling service) before
  returning a blank 500 response.  The `handle-exception` function
  takes two arguments: the exception and the request (which has a
  ready-to-slurp body)."
  [handler handle-exception]
  ;; Note that, as a result of this top-level approach to
  ;; error-handling, the request map sent to Rollbar will lack any
  ;; information added to it by one of the middleware layers.
  (let [request-copy (atom nil)
        handler (with-request-copy handler request-copy)]
    (fn [request]
      (try
        (handler request)
        (catch Throwable e
          (.reset (:body @request-copy))
          ;; You may also want to wrap this line in a try/catch block.
          (handle-exception e @request-copy)
          {:status 500})))))
like image 21
Jeff Terrell Ph.D. Avatar answered Oct 22 '22 04:10

Jeff Terrell Ph.D.