What is an Interceptor?


It doesn’t take very long to bump into the word "Interceptor" when you’re working with Pedestal. They’re the most important construct in the library. Pretty much everything is an interceptor, including some of the things that would normally be built in to a framework.

What You Will Learn

After reading this guide, you will be able to:

  • Explain the role of interceptors in Pedestal

  • Compare interceptors to middleware

  • Create interceptors that affect execution of remaining interceptors

  • Handle errors

Guide Assumptions

This guide is for intermediate users who have worked through some sample applications or the first few Hello World guides. In particular, you should know how to run a build, start a REPL, and start and stop your server.

You do not need to know any other Clojure web frameworks, but if you do, you may find some of the comparisons useful. If not, that’s OK, just skip those sections.

Getting Help if You’re Stuck

If you get stuck at any point in this guide, please submit an issue about this guide or hop over to the mailing list and raise your hand there. You can often find help in the "pedestal" channel of the Clojurians Slack team.

Where We Are Going

We’re going to start with a fresh, empty project in this guide, with just one interceptor. It will help illustrate the concepts we dive into next. After that, it’s time to talk about the execution model and see how we can make interceptors that change the request-handling process dynamically.

Finally, we have to talk about error handling.

Before We Begin

Make sure you have Leiningen installed. We’ll be using it to run our project and to build a project from a template later.

Feel free to consult the full source in the repository, but be warned that the file contains all the versions that we built up through the previous guide. You’ll need to navigate some magic comments to pare it down to just the final version.

A Place to Put Things

We need a project to contain our interceptors while we build them. For now, let’s make a project by hand so we understand each part. (In a real project, you would probably use the Leiningen template to create your new project.)

Make a directory for your project and drop this into project.clj:

(defproject io.pedestal/what-is-an-interceptor "0.5.7"
  :description "Sample project for the What is an Interceptor guide"
  :url ""
  :dependencies [[org.clojure/clojure "1.10.1"]
                 [io.pedestal/pedestal.service "0.5.7"]
                 [io.pedestal/pedestal.jetty "0.5.7"]
                 [org.slf4j/slf4j-api "1.7.28"]
                 [ch.qos.logback/logback-classic "1.2.3" :exclusions [[org.slf4j/slf4j-api]]]])

This is as basic as it gets, but it’s enough to run lein repl and get to work. Go ahead and start a REPL.

Do take note of the two dependencies about logging. If you leave these out, then you won’t get any kind of logging, because slf4j will use its "no-op" logger.

Make a file under src/service.clj with the following contents:

(ns intc.service
  (:require [io.pedestal.http :as http]
            [io.pedestal.interceptor.chain :as chain]
            [io.pedestal.interceptor.error :as err]))

(def routes #{})                                                                     (1)

(defn start
  (-> {::http/port   8822                                                            (2)
       ::http/join?  false                                                           (3)
       ::http/type   :jetty                                                          (4)
       ::http/routes routes}                                                         (5)
      http/create-server                                                             (6)
      http/start))                                                                   (7)
1 We will fill in a route shortly
2 Run on port 8822
3 Give back our REPL thread after starting
4 Use Jetty
5 Use the routes from <1>
6 Expand this "starter" service map into a ready-to-run map
7 Run it

Load that into your REPL and run start. Be sure to bind the return value from start to something so you can stop it later.

Interceptor From Map

An interceptor is a value. That means it acts like any other value in Clojure: you can pass it to a function or get it back as a return value. You could put it in an atom, a ref, an agent, or pass it on a channel.

The simplest way to create an interceptor is by just making a map. Any map that has at least one key of :enter, :leave, or :error can be used as an interceptor. Each of those keys should have a function of one variable.

We’ll create a really basic interceptor that responds to every HTTP request with "Hello, world!".

(def say-hello
  {:name ::say-hello
   :enter (fn [context]
            (assoc context :response {:body "Hello, world!"
                                      :status 200}))})

There are a couple of things to point out here. First, this is defining a var in the namespace and binding it to the value in the map. That means the map is evaluated at compile time.

Second, the map has a :name key. That’s not one of the required keys, but if it exists, Pedestal will print it in some debug messages and logs. It can help you narrow down a problem later.

The :enter key is where things get a little more interesting. (A little, but not much. It’s just "hello world" after all.) Note that it has a function that assocs a value onto the context under the :response key. That value happens to be shaped in a way that the Servlet Interceptor looks for.

This is where we have to start being precise about the difference between an interceptor and how it gets invoked. "Interceptor" is a very general concept. It can be used in a lot of different ways, not just for HTTP handling in a web framework. In fact, HTTP handling comes from the pedestal.service module. Interceptor invocation and handling comes from pedestal.interceptor which doesn’t depend at all on pedestal.service.

For example, there is a project that lets you invoke a chain of interceptors when a message arrives on a Kafka topic. You could invoke interceptors to do batch processing. Generally speaking, interceptors are an implementation of the "pipes and filters" architecture pattern.

Pedestal arranges to call interceptors on one of three functions at different times:

1. Enter

The :enter function is called on the "way in" to a bunch of interceptors. If you look at a route with a collection of interceptors, they’ll have their :enter functions invoked from left-to-right. Each one receives the new value of the context returned by the previous one.

2. Leave

The :leave function is called on the "way out" of a bunch of interceptors. That same collection of interceptors on a route will be called from right-to-left. Like the enter functions, each leave function receives the context and returns a (possibly modified) context.

3. Error

The :error function is a bit special. If an interceptor throws an exception, then Pedestal starts looking for an interceptor with an :error function to handle it. This goes from right-to-left like the :leave functions. The main difference is that an error-handling interceptor may indicate that the error is totally resolved and Pedestal will resume looking for :leave functions. See [Error Handling] below for all the gory details.

The Queue and the Stack

Pedestal starts running interceptors when you call the io.pedestal.interceptor.chain/execute function. You can call execute with an empty context and a collection of interceptors, or you can call it with a context that already has interceptors enqueued. (In the former case, Pedestal just enqueues the interceptors on the context then calls the second form.)

Suppose we start with three interceptors in the queue, like this.

interceptor queue and stack in context 1

Pedestal needs to call the :enter function on "Intc 1". So it pops that interceptor from the queue and moves it to the stack. Then it calls the interceptor, passing the context map itself. This is the context as it appears to "Intc 1".

interceptor queue and stack in context 2

When that’s done, the next thing is to call "Intc 2". Same thing happens, Pedestal pops that interceptor from the queue and pushes it on the stack.

interceptor queue and stack in context 3

Repeat the process for "Intc 3" and we’re left with this context map.

interceptor queue and stack in context 4

Manipulating the Queue

When Pedestal calls an interceptor, the queue and stack aren’t just there for informational use. Interceptors are totally allowed to change things up. Let’s work through an example where we dynamically decide which interceptor to add, depending on a query parameter.

First we’ll change service.clj to add two new claims beyond "Hello World."

(def odds
  {:name ::odds
   :enter (fn [context]
            (assoc context :response {:body "I handle odd numbers\n"
                                      :status 200}))})

(def evens
  {:name ::evens
   :enter (fn [context]
            (assoc context :response {:body "Even numbers are my bag\n"
                                      :status 200}))})

These interceptors aren’t very interesting, but there are two of them. Here’s how we are going to choose which one to run.

(def chooser
  {:name  ::chooser
   :enter (fn [context]
              (let [param (get-in context [:request :query-params :n])               (1)
                    n     (Integer/parseInt param)
                    nxt   (if (even? n) evens odds)]
                (chain/enqueue context [nxt]))                                       (2)
              (catch NumberFormatException e
                (assoc context :response {:body   "Not a number!\n"                  (3)
                                          :status 400}))))})

(def routes
  #{["/hello"        :get say-hello]
    ["/data-science" :get chooser]})
1 Look at the n query parameter from the URL.
2 Enqueue one or the other of our interceptors.
3 Generate a response if neither of the interceptors apply.

Be careful with the call to enqueue. It’s meant to add many interceptors at once, so it takes a collection as the second argument.

Restart your server (since we’re not running in dev mode, we don’t get automatic reloading) and try it out with cURL. You should be able to get any of the three responses. If you get an internal server error, it means there’s something wrong in the code.

Interceptors or Middleware

The very first motivation for Pedestal was the ability to make on-the-fly decisions about how to handle requests. Middleware models wrap up the whole processing chain in function closures. Not only are they opaque, but the decisions are all made at compile time. In contrast, Pedestal treats the processing chain like a virtual call stack, or a malleable program to execute.

Some Practical Applications

Pedestal routers are just interceptors. A router inspects the incoming request and decides what interceptors to enqueue. That’s it! There’s no magic at all in the router. You can build your own replacement router. For that matter, you can build a secondary router that uses something other than the request path to dispatch on.

Suppose you need to add entity-level access control to an API. You can create an interceptor that looks at the entity to decide if authorization is needed. If so, enqueue an interceptor that does the auth check or redirects.

Most controller logic takes this form:

  1. Get information from the request.

  2. If the request can’t be done, generate an error response.

  3. Fetch some data.

  4. Decide what operation to perform on the data.

  5. Decide if the operation can be done on the data.

  6. If so, attempt an operation on the data.

  7. If not, generate an error response.

  8. Attempt to store some data.

  9. If it fails, generate an error response.

  10. Otherwise, generate a response.

Every place the words "decide" or "if" appear here, you could enqueue different interceptors. This breaks the controller logic down into a handful of very small pieces that each consume and produce context maps. They are easily tested in isolation from a database or external service.

You can create a state machine, where one interceptor examines the current state, then enqueues an interceptor appropriate to handle that state.

Once you start looking at the interceptor queue like a program and the stack like a program stack, you’ll find many ways to use dynamic dispatching.

Error Handling

Our data "science" interceptor is pretty small, but it still does just a bit too much work. We’d like to separate error handling from the main flow of logic. What happens if we just let chooser throw an exception?

(def chooser2
  {:name  ::chooser
   :enter (fn [context]
            (let [n (-> context :request :query-params :n Integer/parseInt)
                  nxt (if (even? n) evens odds)]
              (chain/enqueue context [nxt])))})

(def routes
  #{["/hello"        :get say-hello]
    ["/data-science" :get chooser2]})

We get the dreaded "Internal server error." We can do much better if we create an error handler. Any interceptor with an :error function gets asked (very politely) if it can handle the exception. Pedestal does this by calling the :error function with the context map and an ex-info that wraps the original exception. (See the error handling reference for all the gory details.)

We can make an interceptor that handles NumberFormatException like this:

(def number-format-handler
  {:name ::number-format-handler
   :error (fn [context exc]
            (if (= :java.lang.NumberFormatException (:exception-type (ex-data exc))) (1)
                (assoc context :response {:body   "Not a number!\n" :status 400})    (2)
                (assoc context :io.pedestal.interceptor.chain/error exc)))})         (3)

(def routes
  #{["/hello"        :get say-hello]
    ["/data-science" :get [number-format-handler chooser2]]})
1 Check what type of exception was originally thrown.
2 Create a response for our specific error of interest.
3 Tell Pedestal to keep looking for an error handler for anything else. (I.e., "rethrow" the exception.)

This works, but it’s still a bit noisy. Let’s rewrite it using error-dispatch to create the interceptor for us.

(def errors
  (err/error-dispatch [ctx ex]

   [{:exception-type :java.lang.NumberFormatException}]                              (1)
   (assoc ctx :response {:status 400 :body "Not a number!\n"})))                     (2)

(def routes
  #{["/hello"        :get say-hello]
    ["/data-science" :get [errors chooser2]]})
1 Use pattern matching to select which exceptions we handle.
2 Generate a response. Any exceptions that don’t match a pattern automatically get rethrown.

Much nicer!

The Path So Far

In this guide, we’ve gotten into some of the deep inner workings of Pedestal. You’ve learned:

  • How Pedestal invokes interceptors.

  • How to manipulate the interceptor queue and stack.

  • How to split your application logic into smaller pieces using interceptors.

  • How to handle errors

Where To Next?

You may want to head over to some of the references for full details: