Skip to content

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

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions