Explore the intricacies of creating custom interceptors in Pedestal, focusing on their structure, state management, and practical applications in enterprise integration.
In the world of web development with Clojure, Pedestal stands out for its powerful and flexible architecture, largely due to its use of interceptors. Interceptors are the backbone of Pedestal’s request processing pipeline, allowing developers to implement cross-cutting concerns such as logging, authentication, and data transformation in a modular and reusable way. This section delves into the creation of custom interceptors, exploring their structure, state management, and the impact of their ordering on application behavior.
An interceptor in Pedestal is a map that can contain up to three keys: :enter
, :leave
, and :error
. Each of these keys corresponds to a function that takes a context map as an argument and returns a modified context map. Let’s break down each component:
:enter
: This function is invoked when a request enters the interceptor. It is typically used for initial processing, such as extracting data from the request or performing authentication checks.
:leave
: This function is called when the request is leaving the interceptor, usually after the main processing logic has been executed. It is often used for tasks like response modification or logging.
:error
: This function is triggered if an error occurs during the processing of the request. It allows for custom error handling and recovery strategies.
Here’s a basic template for a custom interceptor:
(def my-interceptor
{:enter (fn [context]
;; Enter logic here
context)
:leave (fn [context]
;; Leave logic here
context)
:error (fn [context exception]
;; Error handling logic here
context)})
The context map is a central concept in Pedestal’s interceptor model. It serves as a shared state that can be passed between interceptors, allowing them to communicate and share data. The context map typically contains:
:request
: The original HTTP request map.:response
: The HTTP response map, which can be modified by interceptors.:path-params
: Parameters extracted from the request path.:query-params
: Parameters extracted from the query string.:form-params
: Parameters extracted from the form data.To pass data between interceptors, you can add custom keys to the context map. Here’s an example of how to add and retrieve data:
(def add-user-id-interceptor
{:enter (fn [context]
(assoc context :user-id (get-in context [:request :headers "user-id"])))
:leave (fn [context]
(let [user-id (:user-id context)]
(println "User ID:" user-id)
context))})
In this example, the :enter
function extracts a user ID from the request headers and adds it to the context map. The :leave
function then retrieves and logs this user ID.
A logging interceptor is a common requirement in web applications, providing insights into request and response data. Here’s a simple logging interceptor:
(def logging-interceptor
{:enter (fn [context]
(println "Request:" (:request context))
context)
:leave (fn [context]
(println "Response:" (:response context))
context)})
This interceptor logs the request when it enters and the response when it leaves.
Authentication is a critical aspect of web applications. An authentication interceptor can check for valid credentials and halt the request processing if authentication fails:
(defn authenticate [context]
(let [auth-header (get-in context [:request :headers "authorization"])]
(if (valid-auth? auth-header)
context
(assoc context :response {:status 401 :body "Unauthorized"}))))
(def auth-interceptor
{:enter authenticate})
In this example, the authenticate
function checks the Authorization
header and either allows the request to proceed or returns a 401 Unauthorized response.
Sometimes, you need to transform request or response data. Here’s an interceptor that transforms JSON request bodies into Clojure maps:
(def json-body-interceptor
{:enter (fn [context]
(let [body (slurp (get-in context [:request :body]))]
(assoc-in context [:request :json-body] (json/parse-string body true))))
:leave (fn [context]
(let [response-body (get-in context [:response :body])]
(assoc-in context [:response :body] (json/generate-string response-body))))})
This interceptor reads the request body, parses it as JSON, and adds it to the context map. It also serializes the response body back to JSON.
The order in which interceptors are executed can significantly affect application behavior. Interceptors are executed in the order they appear in the interceptor chain, with :enter
functions being called first, followed by the main processing logic, and then :leave
functions in reverse order.
Consider the following interceptor chain:
(def service-interceptors
[logging-interceptor
auth-interceptor
json-body-interceptor])
In this chain, the logging interceptor will log the request first, followed by the authentication check. If authentication fails, the request processing will halt, and the JSON body interceptor will not be executed. If authentication succeeds, the JSON body interceptor will parse the request body.
Sometimes, you may want to execute an interceptor conditionally based on certain criteria. You can achieve this by wrapping the interceptor logic in a conditional statement:
(defn conditional-interceptor [condition-fn interceptor]
{:enter (fn [context]
(if (condition-fn context)
((:enter interceptor) context)
context))
:leave (fn [context]
(if (condition-fn context)
((:leave interceptor) context)
context))
:error (fn [context exception]
(if (condition-fn context)
((:error interceptor) context exception)
context))})
This function takes a condition function and an interceptor, executing the interceptor only if the condition is met.
For complex applications, you might want to compose multiple interceptors into a single unit. This can be done by creating a composite interceptor that combines the logic of several interceptors:
(def composite-interceptor
{:enter (fn [context]
(-> context
(first-interceptor :enter)
(second-interceptor :enter)))
:leave (fn [context]
(-> context
(second-interceptor :leave)
(first-interceptor :leave)))
:error (fn [context exception]
(-> context
(first-interceptor :error exception)
(second-interceptor :error exception)))})
This composite interceptor applies the :enter
, :leave
, and :error
functions of two interceptors in sequence.
Creating custom interceptors in Pedestal is a powerful way to manage cross-cutting concerns in your web applications. By understanding the interceptor structure, effectively managing state with the context map, and carefully considering interceptor ordering, you can build robust and maintainable applications. Whether you’re implementing logging, authentication, or data transformation, interceptors provide the flexibility and modularity needed for enterprise integration.