5

How can I safely get the content of the :body InputStream from compojure?

See related but different question for background.

I'm trying to authenticate my ring routes with Friend using compojure handler/site but when I try to read the :body from an http POST request (which is a Java InputStream), it is closed:

23:01:20.505 ERROR [io.undertow.request] (XNIO-1 task-3) Undertow request failed HttpServerExchange{ POST /paypal/ipn}
java.io.IOException: UT000034: Stream is closed
    at io.undertow.io.UndertowInputStream.read(UndertowInputStream.java:84) ~[undertow-core-1.1.0.Final.jar:1.1.0.Final]
    at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:284) ~[na:1.8.0_45]
    at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:326) ~[na:1.8.0_45]
    at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178) ~[na:1.8.0_45]
    at java.io.InputStreamReader.read(InputStreamReader.java:184) ~[na:1.8.0_45]
    at java.io.BufferedReader.fill(BufferedReader.java:161) ~[na:1.8.0_45]
    at java.io.BufferedReader.read(BufferedReader.java:182) ~[na:1.8.0_45]
    at clojure.core$slurp.doInvoke(core.clj:6650) ~[clojure-1.7.0-beta1.jar:na]
    at clojure.lang.RestFn.invoke(RestFn.java:410) ~[clojure-1.7.0-beta1.jar:na]

If I remove the handler, the problem goes away. I've found one possible solution called groundhog that captures and stores all requests. The library I'm using, clojure-paypal-ipn originally called reset on the stream, but that is not supported by Undertow (or indeed several other Java/Clojure servers), so I worked around it.

Here is a related discussion with weavejester, author of compojure.

Here are some snippets of my code:

(defroutes routes
  ...
  (POST "/paypal/ipn" [] (payment/paypal-ipn-handler 
                          payment/paypal-data 
                          payment/paypal-error 
                          paypal-sandbox?))
  (route/resources "/"))

(defn authenticate-routes
  "Add Friend handler to routes"
  [routes-set]
  (handler/site
    (friend/authenticate routes-set friend-settings)))

;; middleware below from immutant.web.middleware
(defn -main [& {:as args}]
  (web/run
    (-> routes
      (web-middleware/wrap-session {:timeout 20})

      (authenticate-routes) ; use friend handler

      ;; wrap the handler with websocket support
      ;; websocket requests will go to the callbacks, ring requests to the handler
      (web-middleware/wrap-websocket websocket-callbacks))
    args))

And here are the guts of payment.clj (paypal-data and paypal-error just pprint input right now):

(defn req->body-str [req]
  "Get request body from ring POST http request"
  (let [input-stream (:body req)]
      (let [raw-body-str (slurp input-stream)]
          raw-body-str)))

(defn paypal-ipn-handler
  ([on-success on-failure] (paypal-ipn-handler on-success on-failure true))
  ([on-success on-failure sandbox?]
   (fn [req]
     (let [body-str (req->body-str req)
           ipn-data (paypal/parse-paypal-ipn-string body-str)]
       (do
         (.start (Thread. (fn [] (paypal/handle-ipn ipn-data on-success on-failure sandbox?))))
         ; respond to PayPal right away, then go and process the ipn-data
         {:status  200
          :headers {"Content-Type" "text/html"}
          :body    ""})))))
Community
  • 1
  • 1
sventechie
  • 1,859
  • 1
  • 22
  • 51
  • 1
    Could you show the exact route definition, how you wrap the call with friend? – schaueho Apr 25 '15 at 16:14
  • I've updated the question with some of my code, incl. routes. I'm building from the [immutant 2.0 feature demo](https://github.com/immutant/feature-demo/). – sventechie Apr 25 '15 at 21:54
  • FWIW, I don't see anything obviously wrong with your code. I have a somewhat different setup (no immutant), but at least I can tell you that there is no problem in using POST request with compojure and friend. – schaueho Apr 26 '15 at 19:28
  • It seems compojure `handler/site` is [deprecated](https://github.com/weavejester/compojure-template/issues/16). I guess I should use [ring-defaults](https://github.com/ring-clojure/ring-defaults) instead. – sventechie Apr 27 '15 at 15:15
  • It appears this problem is actually related to ring anti-forgery. I've [made a new question here](http://stackoverflow.com/questions/30172569/clojure-anti-forgery-csrf-token-invalid-with-latest-version-ring-compojure). – sventechie May 11 '15 at 16:20
  • I've had exactly the same problem with reset. Did you find a solution in the end? – yazz.com Dec 01 '15 at 10:24
  • Jeremy Vuillermet said he used used the `body-string` function from `ring.util.request` to get the data despite the stream reset. – sventechie Dec 01 '15 at 17:51
  • To be specific, he says ''Yes, I copied the wrapper code and replaced `req->raw-body-str` with `body-string`" – sventechie Dec 01 '15 at 18:12

0 Answers0