Skip to content

:interceptors does not flatten vectors like :middleware -> cannot succinctly compose chains of interceptors #707

@Ramblurr

Description

@Ramblurr

Given:

(require
   '[reitit.http :as http]
   '[reitit.http.coercion :as coercion]
   '[reitit.http.interceptors.parameters :as parameters])

(def api-chain [parameters/parameters-interceptor
                coercion/coerce-response-interceptor
                coercion/coerce-request-interceptor])

(def my-interceptor
  {:enter (fn [ctx] ctx)})

(http/router ["/" {:interceptors [api-chain my-interceptor]}])

Result:

   Wrong number of args (2) passed to:
   reitit.http.interceptors.parameters/parameters-interceptor
   {:reitit.exception/cause #error {
    :cause "Wrong number of args (2) passed to: reitit.http.interceptors.parameters/parameters-interceptor"
    :via
    [{:type clojure.lang.ArityException
      :message "Wrong number of args (2) passed to: reitit.http.interceptors.parameters/parameters-interceptor"
      :at [clojure.lang.AFn throwArity "AFn.java" 429]}]
    :trace
    [[clojure.lang.AFn throwArity "AFn.java" 429]
     [clojure.lang.AFn invoke "AFn.java" 36]
     [clojure.lang.AFn applyToHelper "AFn.java" 156]
     [clojure.lang.AFn applyTo "AFn.java" 144]
     [clojure.core$apply invokeStatic "core.clj" 667]
     [clojure.core$apply invoke "core.clj" 662]
     [reitit.interceptor$eval46785$fn__46787 invoke "NO_SOURCE_FILE" 58]
     [reitit.interceptor$eval33607$fn__33608$G__33596__33617 invoke "interceptor.cljc" 7]
     [reitit.interceptor$chain$fn__34004 invoke "interceptor.cljc" 113]
...

Expected result:

I expect the resulting interceptors chain for "/" to be:

[parameters/parameters-interceptor
  coercion/coerce-response-interceptor
  coercion/coerce-request-interceptor
  my-interceptor]

Extra

@ikitommi indicated that this should work:
image
(slack link)

However, looking at the implementation, it cannot work, because the :interceptors vector in route data is processed one element at a time:

(defn chain
"Creates a Interceptor chain out of sequence of IntoInterceptor
Optionally takes route data and (Router) opts."
([interceptors]
(chain interceptors nil nil))
([interceptors data]
(chain interceptors data nil))
([interceptors data {::keys [transform] :or {transform identity} :as opts}]
(let [transform (if (vector? transform) (apply comp (reverse transform)) transform)]
(->> interceptors
(keep #(into-interceptor % data opts))
(transform)
(keep #(into-interceptor % data opts))
(into [])))))

So it is not possible for the into-interceptor implementation for clojure.lang.APersistentVector to flatten the vector, instead it assumes you're doing a delayed function call: [some-interceptor-ctor :a-param :another-param] -> (some-interceptor-ctor :a-param :another-param) (I'm curious what the usecase for that is btw?)

Workarounds

We would like this feature, as defining a stack, or chain, of interceptors that can be re-used and composed is very useful.

Unfortunately there aren't any easy workarounds.

  1. :reitit.interceptor/transform i.e., (http/router ["/" {:interceptors [api-chain my-interceptor]}] {:reitit.interceptor/transform custom-transform)

    Not possible. Because into-interceptor is run on the interceptors vector once before the transformation (see (defn chain..) above).

  2. :compile i.e., (http/router ["/" {:interceptors [api-chain my-interceptor]}] {:compile custom-compile-fn)
    This is technically possible, but requires redefining:

    ...oof 😨

  3. One simple workaround of course is to:

    ["/" {:interceptors (conj api-chain my-interceptor) }]

    Or for composing multiple chains (also very common):

    ["/" {:interceptors (concat api-chain auth-chain [my-interceptor]) }]

    For experienced clojure developers the above two are of course perfectly serviceable, but a far cry from the readability of the following. When teaching new clojure devs about web dev, one of the first files they see is the routes file and the low-level noise of concat is distracting from higher level concepts.

    Ideally we could write:

    ["/" {:interceptors [api-chain auth-chain my-interceptor])}]
    ;; where api-chain and auth-chain are vectors of IntoInterceptors, and my-interceptor is an IntoInterceptor

While I'm wishing, it would be even nicer if the api-chain (aka vectors of IntoInterceptors) could be put in the registry, allowing:

["/" {:interceptors [:my.proj/api-chain :my.auth/auth-chain my-interceptor])}]

And I am pretty sure this would work out-of-the-box, if one IntoInterceptor could expand into multiple IntoInterceptors. Because registry is already defined as a map of keyword => IntoInterceptor.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    Status

    Not right now

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions