当已经读取请求体时如何读取阅读环的请求体

13

我的问题是,如果Ring请求的主体已经被读取,我该如何习惯性地阅读它?

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

具体来说,发生的情况是某些中间件(在我这种情况下是ring.middleware.json/wrap-json-body中间件)对主体InputStream对象进行了一次slurp,这会更改对象的内部状态,从而使将来的调用slurp返回一个空字符串。因此,请求映射中的主体[内容]实际上已丢失。

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

3个回答

6

我有一个库,可以将请求体替换为具有相同内容的流,并存储原始请求体以便稍后进行压缩。

地鼠

如果需要无限期开放的流,则这种方法不够好,如果请求体是某个大型对象的上传,则是一个坏主意。但是,它有助于测试和作为调试过程的一部分重现错误条件。

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


我采用的方法基于tee-stream。感谢您提供这个,以及groundhog。我接受了这个答案,并将详细说明我的方法在另一个答案中。 - Jeff Terrell Ph.D.

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

也许我太蠢了,但我不太明白这是如何工作的。tee-stream 的整个重点在于它返回流的内容和一个新流,但你忽略了返回的内容,那么它到底实现了什么? - Andy
已经有一段时间了,但我相信重要的部分是 :streamByteArrayInputStream 的一个实例,可以像 wrap-error-page 一样进行 .reset - Jeff Terrell Ph.D.

1
我认为您被某种“保留备份以防万一”的策略所困扰。不幸的是,请求中的:body必须是InputStream,而且没有其他选项(响应中可以是String或其他内容,这就是我提到它的原因)。
简述:在非常早期的中间件中,将:body InputStream包装在在关闭时重置自身的InputStream中(示例)。并非所有的InputStream都可以重置,因此您可能需要进行一些复制。一旦包装,流就可以在关闭时重新读取,并且您就完成了。如果您有巨大的请求,则存在内存风险。
更新:这里是一个半成品尝试,部分灵感来自于groundhog中的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))))

好主意;这种方法将允许透明地重新读取。不幸的是,实际的“InputStream”对象更具体地是一个“org.eclipse.jetty.server.HttpInput”对象,它是不可重置的。但我认为你的方法是正确的。如果您能勾画出在无法重置的情况下有效的解决方案,或者如果在几天内没有其他人做同样的事情,我将接受这个答案。 - Jeff Terrell Ph.D.
@JeffTerrell 我认为你可以将 HttpInput 包装在 BufferedInputStream 中,然后再将其包装在可重置的流中。我很好奇,打算试一下。 - overthink
clojure.java.io/input-stream 应该会为你返回一个 BufferedInputStream。 - Alex
我喜欢用可重置的输入流替换输入流的建议。我可能会借鉴这个想法到我的土拨鼠项目中(目前我存储/压缩,但自动重置似乎是一个不错的行为)。 - noisesmith
@noisesmith 我正在思考这种方法的一个缺点:谁实际上关闭原始流?它被包装在ResettableStream中,其close()被覆盖。我认为我更喜欢你的tee-stream方法! - overthink
我认为你的方法可行。感谢你详细说明。很抱歉要反悔,但我接受了另一个答案,因为它更简单、更清晰。 - Jeff Terrell Ph.D.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接