reitit

2024-10-18T06:38:54.035359Z

I use ring + reitit. How would I write a middleware that adds a computed https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Digest ? For verification purposes. What I know: • how to calculate the digest, given a body • that the :body of a request is an InputStream and so has to be consumed at most once What I don’t know: • how to play well with other middlewares • I have other middlewares that consume the body already and turn it into Clojure data structures - I assume this is the https://github.com/metosin/reitit/blob/master/doc/ring/coercion.md. But I need bytes to compute the digest (order doesn’t matter in hash-maps but it does for a content-digest). A naive version that does not care would look like this but of course this wouldn’t work.

(defn calculate-content-digest [request]
  (let [digest (calculate-digest ^InputStream (:body request))] ;; assumes body has not been consumed (not true in my case, it already has been coerced), and consumes it (won't allow downstream consumers to coerce)
    (assoc request :content-digest (str "sha-512=:" (codec/->base64 digest) ":")))) 

jussi 2024-10-18T09:48:38.274119Z

IIRC you have to make an actual copy of the InputStream in order to use it without consuming it. We add simple version header to response like this

(def get-version (memoize get-version*))

(defn create-version-header-middleware*
  "Create middleware fn that adds application version to response headers"
  [handler request]
  (let [response (handler request)]
    (rresponse/header response "X-Version" (get-version))))
And we just add it to a routes :middleware stack.

2024-10-18T10:04:11.177519Z

Right I was thinking about a copy as well. This should work and I might end up doing this. But I was wondering if there was a way to avoid the copy, maybe computing the digest at the same time as the coercion happens.

jussi 2024-10-18T10:21:34.238549Z

You could define your own coercion for :body as it is configurable for reitit. This is the default coercion for body

:transformers {:body {:default default-transformer-provider
                         :formats {"application/json" json-transformer-provider}}
and I think it can be overridden in config for :coercion by providing the key :transformers with an implementation (depending ofc which coercion you are using, we use malli)
;; Route options
    {:conflicts (constantly nil) ;; allow the catch-all path for SPA
     :data {:muuntaja muuntaja/instance
            :middleware [wrap-internal-error
                         swagger/swagger-feature
                         middleware.muuntaja/format-negotiate-middleware
                         middleware.muuntaja/format-response-middleware
                         middleware.muuntaja/format-request-middleware
                         middleware.parameters/parameters-middleware
                         coercion/coerce-request-middleware]
            :coercion (reitit.coercion.malli/create
                       {;; set of keys to include in error messages
                        :error-keys #{:value :humanized #_:type :errors  #_:transformed}
                        ;; validate request & response
                        :validate true
                        ;; top-level short-circuit to disable request & response coercion
                        :enabled true
                        ;; strip-extra-keys (effects only predefined transformers)
                        :strip-extra-keys false
                        ;; add/set default values
                        :default-values true
                        ;; malli options
                        :options nil})}
     :exception pretty/exception}

jussi 2024-10-18T10:22:22.194839Z

But I think that the copying is quite simple solution albeit it might consume some memory.

juhoteperi 2024-10-18T11:01:41.496369Z

Coercion doesn't read the request body, Muuntaja will read the body. Maybe you can add one mw to setup your InputStream before Muuntaja so that you have the digest available when the stream is read, then second mw to check the result after Muuntaja has read the body.

✅ 1
Ben Sless 2024-10-18T11:04:40.625189Z

Add a middleware that consumes the body to a byte array, and assoc it twice into the request, once as body and another was whatever you need for the digest calculation

juhoteperi 2024-10-18T11:05:30.998049Z

Something like this

(defn setup-digest-input-stream [handler]
  (fn [req]
    (let [dis (DigestInputStream. (:body req))]
      (handler (assoc req :body dis ::dis dis)))))

(defn check-digest [handler]
  (fn [req]
    (handler (assoc req :digest (.getMessageDigest (::dis req))))))

:middlware [setup-digest-input-stream
            middleware.muuntaja/format-request-middleware
            check-digest]

👀 1
juhoteperi 2024-10-18T11:06:11.469739Z

the digest should be available to the second mw, as Muuntaja has read the stream

juhoteperi 2024-10-18T11:12:42.317369Z

looks like DigestInputStream also takes the MessageDigest instance as a parameter etc, but the idea is the same

2024-10-18T12:40:42.333019Z

It looks like it should work. I am getting some bytes out of (.digest (.getMessageDigest dis)) I’ll need to check that this is what I expect.

2024-10-18T12:42:36.289619Z

Aside: should I .close my DigestInputStream, or the original body ? and if yes, which middleware should be responsible for this?

juhoteperi 2024-10-18T12:48:48.685139Z

Muuntaja should close the IS it reads and I think DIS closes the original is IS also when it is closed

2024-10-18T12:53:41.248649Z

The second part checks out at least: DIS inherits FilterInputStream https://docs.oracle.com/javase/8/docs/api/index.html?java/io/FilterInputStream.html It would also make sense for Muuntaja to close the :body (otherwise who would (?)). Thanks 🙂

valerauko 2024-10-18T12:58:30.890429Z

Depends a lot on libraries you use but I think if this is an incoming request and you're not actually streaming the body, then you can call .reset on the :body and consume it again

2024-10-18T13:00:19.954729Z

I get a Jetty HttpInputOverHTTP and it doesn’t seem to be possible. DigestInputStream looks like it works and does not depend on being able to consume the stream again.

👍 1