Тело запроса чтения кольца, когда оно уже прочитано

Мой вопрос в том, как я могу идиоматически читать тело запроса кольца, если он уже был прочитан?

Вот фон. Я пишу обработчик ошибок для приложения Ring. Когда возникает ошибка, я хочу зарегистрировать ошибку, включая всю соответствующую информацию, которая может понадобиться для воспроизведения и исправления ошибки. Одной важной частью информации является тело запроса. Однако проблема с состоянием значения :body (потому что это тип объекта java.io.InputStream) вызывает проблемы.

В частности, происходит то, что некоторое промежуточное программное обеспечение (промежуточное программное обеспечение ring.middleware.json/wrap-json-body в моем случае) выполняет slurp на теле InputStream объект, который изменяет внутреннее состояние объекта, так что будущие вызовы slurp возвращает пустую строку. Таким образом, [содержимое тела] эффективно теряется из карты запросов.

Единственное решение, о котором я могу думать, - это упреждающее копирование объекта body InputStream до того, как тело может быть прочитано, на случай, если оно мне понадобится позже. Мне не нравится этот подход, потому что кажется неуклюжим делать какую-то работу над каждым запросом, если в дальнейшем может возникнуть ошибка. Есть ли лучший подход?

Ответ 1

У меня есть lib, который всасывает тело, заменяет его потоком с идентичным содержимым и сохраняет оригинал, чтобы впоследствии его можно было спутать.

groundhog

Это недостаточно для бесконечно открытых потоков, и это плохая идея, если тело - это загрузка какого-либо крупного объекта. Но это помогает для тестирования и воссоздания условий ошибки в рамках процесса отладки.

Если все, что вам нужно, это дубликат потока, вы можете использовать функцию tee-stream от сурка в качестве основы для вашего собственного промежуточного программного обеспечения.

Ответ 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})))))

Ответ 3

Я думаю, вы застряли с какой-то стратегией "держите копию на всякий случай". К сожалению, он выглядит как :body по запросу должен быть InputStream и ничего больше (в ответе это может быть String или другие вещи, поэтому я упоминаю об этом)

Эскиз. В очень раннем промежуточном программном обеспечении оберните InputStream :body в InputStream, который сбрасывает себя при закрытии (пример). Не все InputStream могут быть reset, поэтому вам может понадобиться сделать некоторое копирование здесь. После обертывания поток можно перечитать на близком расстоянии, и вы добры. Там риск памяти здесь, если у вас есть гигантские запросы.

Обновление: здесь полузапеченная попытка, частично вдохновленная 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))))