2013-12-12 48 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})))))