已阅读时读取环请求正文

时间:2013-12-12 20:47:04

标签: clojure ring

我的问题是,如果已经读取了Ring请求的主体,我怎么能惯用它?

这是背景资料。我正在为Ring应用程序编写错误处理程序。发生错误时,我想记录错误,包括我可能需要重现的所有相关信息并修复错误。一个重要的信息是请求的主体。但是,:body值的有状态(因为它是java.io.InputStream对象的类型)会导致问题。

具体来说,会发生一些中间件(在我的情况下为ring.middleware.json/wrap-json-body中间件)在body slurp对象上执行InputStream,这会更改对象的内部状态,以便将来调用slurp会返回一个空字符串。因此,从请求图中有效地丢失了[内容]。

我能想到的唯一解决方案是在可以读取正文之前抢先复制正文InputStream对象,以防以后我可能需要它。我不喜欢这种方法,因为在以后可能出现错误的情况下,对每个请求做一些工作似乎很笨拙。有更好的方法吗?

3 个答案:

答案 0 :(得分:5)

我有一个吸收身体的lib,用相同内容的流替换它,然后存储原始文件,以便以后可以放气。

groundhog

这对于无限期打开的流是不够的,如果正文是上传某个大对象,那么这是一个坏主意。但它有助于测试,并在调试过程中重新创建错误条件。

如果您需要的只是流的副本,则可以使用groundhog中的tee-stream函数作为您自己的中间件的基础。

答案 1 :(得分:3)

我采用了@noisesmith的基本方法并进行了一些修改,如下所示。这些功能中的每一个都可以用作Ring中间件。

(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})))))

答案 2 :(得分:1)

我认为你会遇到某种“保留副本以防万一”的策略。不幸的是,它在请求must be an InputStream上看起来像:body而没有其他内容(在响应上它可能是String或其他事情,这就是我提到它的原因)

草图:在一个非常早期的中间件中,将:body InputStream包装在InputStream中,在关闭(example)时自行重置。并非所有InputStream都可以重置,因此您可能需要在此处进行一些复制。一旦被包裹,可以在关闭时重新读取流,并且你很好。如果您有巨大的请求,那么这里存在内存风险。

更新:这是一个半生不熟的尝试,部分受到土拨鼠tee-stream的启发。

(require '[clojure.java.io :refer [copy]])
(defn wrap-resettable-body
  [handler]
  (fn [request]
    (let [orig-body (:body request)
          baos (java.io.ByteArrayOutputStream.)
          _ (copy orig-body baos)
          ba (.toByteArray baos)
          bais (java.io.ByteArrayInputStream. ba)
          ;; bais doesn't need to be closed, and supports resetting, so wrap it
          ;; in a delegating proxy that calls its reset when closed.
          resettable (proxy [java.io.InputStream] []
                       (available [] (.available bais))
                       (close [] (.reset bais))
                       (mark [read-limit] (.mark bais read-limit))
                       (markSupported [] (.markSupported bais))
                       ;; exercise to reader: proxy with overloaded methods...
                       ;; (read [] (.read bais))
                       (read [b off len] (.read bais b off len))
                       (reset [] (.reset bais))
                       (skip [n] (.skip bais)))
          updated-req (assoc request :body resettable)]
      (handler updated-req))))