Requests flow in from clients, through the network connector (such as jetty.adoc), which in turn,
delegates to a Pedestal connector to handle the bulk of the request. This processing takes the form of
a series of interceptors, in a specific order, each focused on one small part of the processing of the request.
The interceptor chain manages the execution of the interceptors.
Pedestal is far from the first web framework for Clojure, and attempts to expand on ideas
piloted elsewhere.
Ring is likely the most popular and influential.
In Ring, each route (a combination of an HTTP method and a path pattern) is mapped to a handler:
the handler is just a simple function which accepts a request map as its input, and
returns a response map.
Because a server is more than a single route, and because many routes will share a lot of behavior such as logging,
authentication, response rendering, and parameter parsing, each individual Ring route is wrapped in middleware,
a function that wraps an existing handler and returns a new handler.
Middleware can inspect and modify the incoming request map or the outgoing response map.
For example, perhaps your Ring application needs to respond specially to HEAD requests;
link:example$org/example/middleware.clj[role=include]
1 | This is the returned handler, wrapping around the original handler |
2 | Ring specifies at set of keys for the request map; Pedestal follows the same rules |
3 | Ring specifies another set of keys for the response map |
4 | Here’s where we delegate down to the next handler |
This is good, functional design, but is limited in at least one way: it’s all on the stack of a single request processing thread.
Given how great Clojure is at multithreaded programming, that can be a limitation.
Several of the central features of Pedestal (such as streaming events and web socket connections) are at odds with this:
in these cases a long-running server-side process only occasionally needs a request processing thread to send an event,
or web socket message, to the client.
So, in Pedestal we want something as easily reused and composable as Ring middleware functions, without
the limitations of the stack-of-function-calls invocation model.
That’s interceptors.
An interceptor is a bundle of up-to three functions, named :enter, :leave, and :error.
:enter corresponds to the logic before delegating to the next interceptor; the request can be inspected or modified.
Likewise, :leave corresponds to logic that occurs after a response map has been created.
The :error function is used when an interceptor throws an exception; it’s the interceptor version of a (try … catch …)
.
Now, there’s a bit more going on. First, these :enter and :leave functions are passed a context map which
contains a :request key.
Any interceptor can modify the context map: some may modify the :request map inside the context, others may
attach a :response map to the context.
Inside the context is a queue of interceptors.
Pedestal works its way through the queue, calling :enter functions, until some interceptor attaches a
:response map to the context;
then it works its way backwards, calling :leave functions.
In practice, an interceptor looks like a normal Clojure map with keys :enter, :leave, or :error (usually just :enter and/or :leave).
So, unlike a deeply wrapped function, a chain of interceptors is a data structure that can be inspected by a developer, or manipulated in code.
Importantly, any interceptor can add new interceptors to the queue (because the interceptor queue itself is stored
in the context, alongside the :request map). Because of this,
routing boils down to an interceptor
that peeks at the request and makes decisions about what additional interceptors to add to the queue.
Pedestal includes general purpose interceptors for all sorts of typical HTTP request functionality:
Application logic is also implemented as interceptors, though there’s some simple helpers that allow you to write
these as Ring-style handlers, if you like.
But Pedestal has one more big trick up its sleeve: any interceptor may, instead of returning a context map,
instead return a {core_async} channel that conveys the context map at some point
in the future; this is what allows for asynchronous request processing.
Along with moving core logic into interceptors, we have moved the HTTP
connection handling out of interceptor processing to create an
interface for the chain provider.
The chain provider sets up the initial context and queue of
interceptors. It starts execution.
Pedestal includes a servlet chain provider out of the box. It connects
any servlet container to an interceptor chain. The
api:create-servlet[]
function orchestrates this work. This is strictly a convenience
function that takes a service-map.adoc of everything
needed to run an HTTP service.
It is possible to create other chain providers. The
{repo_root}/tree/master/samples/fast-pedestal[fast-pedestal
sample] shows how to do this with Jetty.
See the following namespaces for the HTTP chain provider: