2013-12-12 51 views
11

我的問題是,我如何才能習慣性地讀取戒指請求的主體,如果它已被閱讀?閱讀戒指請求正文當已經閱讀

這是背景。我正在爲Ring應用程序編寫錯誤處理程序。發生錯誤時,我想記錄錯誤,包括可能需要重現並修復錯誤的所有相關信息。一個重要的信息是請求的主體。但是,:body值的狀態(因爲它是一種java.io.InputStream對象)會導致問題。

具體來說,什麼情況是,一些中間件(在ring.middleware.json/wrap-json-body中間件在我的情況)確實身體InputStream對象,這會改變對象的內部狀態,使得以slurp返回一個空字符串未來調用上slurp。因此,請求地圖中的[body]內容有效丟失。

我能想到的唯一解決方案是在身體可以被閱讀之前搶先複製身體InputStream對象,以防萬一我以後可能需要它。我不喜歡這種方法,因爲在每個請求上做一些工作似乎很笨拙,以防萬一以後出現錯誤。有更好的方法嗎?

回答

6

我有吸取體內,與具有相同內容的流來替換它,並存儲原始以便它可以在以後癟一個lib。

groundhog

這不足以無限期地開放流,是一個壞主意,如果身體的一些大對象的上傳。但它有助於測試,並將錯誤條件重新創建爲調試過程的一部分。

如果您需要的只是流的副本,您可以使用groundhog的tee-stream函數作爲您自己的中間件的基礎。

+0

我採取的方法是基於'tee-stream'。感謝你,並''groundhog'。我接受了這個答案,我將在單獨的答案中詳細說明我的方法。 –

1

我認爲你陷入某種「保持副本以防萬一」的策略。不幸的是,它看起來像:body的要求must be an InputStream,沒有別的(在響應它可以是一個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)))) 
+0

好主意;這種方法將允許透明的重新流通。不幸的是,實際的'InputStream'對象更具體地說是'org.eclipse.jetty.server.HttpInput'對象,它不是'reset'table。但我認爲你的方法是健全的。如果您勾畫出可在不可重置的情況下工作的解決方案,或者在幾天內沒有其他人也這樣做,我會接受此答案。 –

+0

@JeffTerrell我想你可以將HttpInput包裝在BufferedInputStream中,並進一步將其包裝在可重置的對象中。我很好奇,並且會嘗試。 – overthink

+0

clojure.java.io/input-stream應該爲你返回一個BufferedInputStream。 – Alex

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