In case the user is trying to access some path that they’re not allowed into, I want my service to short-circuit and return HTTP status 403 immediately. What is the right way to do this? Should I throw an exception in one interceptor and then handle it in another?
Also, based on http://pedestal.io/guides/hello-world-content-types > One important note here: As soon as any interceptor attaches a response to the context map, Pedestal considers the request handled. Remaining interceptors won’t be called, and only the ones that are already on the stack will have their `:leave` functions called. This is a “short-circuit” behavior like an early-exit in code. I would expect an interceptor chain consisting of two interceptors where the first one attaches a 403 response to short-circuit the chain, but it doesn’t seem to do so? This is the code for the interceptor:
(ic/interceptor
{:name ::session-guard
:enter (fn [ctx]
(assoc ctx :response {:status 403}))})
It is the first interceptor in a vector containing two of them. If it is the only interceptor in the chain I get my 403 response, but otherwise it just continues on the to the next interceptor. Kind of unexpected?This is the relevant route:
#{["/guarded" :get [(ic/session-guard conf) (guarded-page)] :route-name ::guarded-page]}
(ic/session-guard conf) creates the interceptor that attaches a 403 response, while (guarded-page) creates a ring handler that makes a regular 200 response. When both are present I always get the 200 and if only the first interceptor is there I get the 403. No short-circuiting in sight?So I followed the Pedestal source code all the way to
(defn- terminator-inject
[context]
(interceptor.chain/terminate-when context #(ring-response/response? (:response %))))
Which checks whether the response is a valid Ring response:
(defn response?
"True if the supplied value is a valid response map."
{:added "1.1"}
[resp]
(and (map? resp)
(integer? (:status resp))
(map? (:headers resp))))
And then I noticed that :headers must be set, so I went and modified my interceptor to also return a headers map:
(defn session-guard
[{:keys [users]}]
(ic/interceptor
{:name ::session-guard
:enter (fn [ctx]
(assoc ctx :response {:status 403
:headers {}}))}))
and now it short-circuits as expected 🙂
I would still like to know if it makes more sense to do this as part of error-handling though. Any thoughts? Also, sorry for a wall of text.there is a io.pedestal.http/response?
, that is used by the standard not-found-interceptor
😉
actually I discovered that if you just assoc
a response to the context in an interceptor, it stops the chain right there and sends your response
(assoc context :response (u/forbidden msg))
There’s also “terminate” http://pedestal.io/api/pedestal.interceptor/io.pedestal.interceptor.chain.html#var-terminate
but I wonder if that's as valid as I tested
yeah I did see terminate, but why not just set the response if it's enough?
Yeah the :response value should trigger the short-circuiting. I wasn’t aware of the header requirement, but in our services we always attach content-type headers.
1👌To me throwing an error in your interceptor, then catching it in another error interceptor and returning a 403 seems like a good way to do it. Especially if you are in a place where it’s easier to throw an exception than return a response. But would be interesting to know how other people handle that.
1👍We usually check permissions at the very beginning of the interceptor stack.
So don’t find myself needing to do that.
for the header, I use the :leave
interceptor io.pedestal.http/json-body
and never had an issue (I never set them manually)
json-body docs
Set the Content-Type header to "application/json" and convert the body to
JSON if the body is a collection and a type has not been set.
seems kind of sloppy that two only slightly different predicate functions are in use for termination and not-found… wonder why?