【问题标题】:Reading Ring request body when already read已读取时读取 Ring 请求正文
【发布时间】:2013-12-31 11:28:13
【问题描述】:

我的问题是,如果 Ring 请求的正文已被读取,我该如何以惯用方式读取它?

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

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

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

【问题讨论】:

    标签: clojure ring


    【解决方案1】:

    我有一个库,它吸收了主体,将其替换为具有相同内容的流,并存储原始内容,以便以后可以放气。

    groundhog

    这对于无限开放的流来说是不够的,如果主体是一些大对象的上传,这是一个坏主意。但它有助于测试,并在调试过程中重新创建错误条件。

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

    【讨论】:

    • 我采取的方法是基于tee-stream。谢谢你,谢谢groundhog。我接受这个答案,我将在单独的答案中详细说明我的方法。
    【解决方案2】:

    我采用了@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 的全部意义在于它返回流的内容和一个新的流,但是你忽略了返回的内容,那么它实现了什么?
    • 已经有一段时间了,但我认为重要的是:streamByteArrayInputStream 的一个实例,它可以是.reset,就像wrap-error-page 一样。
    【解决方案3】:

    我认为您被某种“保留副本以防万一”的策略所困扰。不幸的是,它在请求 must be an InputStream 上看起来像 :body 而没有别的(在响应中它可以是 String 或其他东西,这就是我提到它的原因)

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

    更新:这是一个半生不熟的尝试,部分灵感来自土拨鼠中的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))))
    

    【讨论】:

    • 好主意;这种方法将允许透明地重新slurping。对我来说不幸的是,实际的InputStream 对象更具体地说是org.eclipse.jetty.server.HttpInput 对象,而不是resettable。但我认为你的方法是合理的。如果您草拟一个适用于不可重置情况的解决方案,或者几天内没有其他人这样做,我会接受这个答案。
    • @JeffTerrell 我认为您可以将 HttpInput 包装在 BufferedInputStream 中,并进一步将其包装在可重置的输入流中。我很好奇,打算试试。
    • clojure.java.io/input-stream 应该为您返回一个 BufferedInputStream。
    • 我喜欢用可重置的输入流替换输入流的建议。我可能会为我的土拨鼠项目借用这个想法(目前我存储/放气,但自动重置似乎是一个不错的行为)。
    • @noisesmith 我正在考虑这种方法的一个缺点:谁真正关闭了原始流?它被包装在其 close() 被覆盖的 ResettableStream 中。我想我更喜欢你的 tee-stream 方法!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2017-03-25
    • 2015-10-02
    • 1970-01-01
    • 2016-03-04
    • 1970-01-01
    • 2017-11-25
    • 2019-01-17
    相关资源
    最近更新 更多