Browse Clojure Frameworks and Libraries: Tools for Enterprise Integration

Creating Custom Interceptors in Pedestal: A Comprehensive Guide

Explore the intricacies of creating custom interceptors in Pedestal, focusing on their structure, state management, and practical applications in enterprise integration.

8.3.2 Creating Custom Interceptors§

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.

Understanding the Interceptor Structure§

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)})

State Management with the Context Map§

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.

Practical Examples of Custom Interceptors§

Logging Interceptor§

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 Interceptor§

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.

Data Transformation Interceptor§

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.

Interceptor Ordering and Its Impact§

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.

Best Practices for Interceptor Ordering§

  • Place authentication and authorization interceptors early in the chain to prevent unauthorized access to resources.
  • Logging interceptors can be placed at the beginning and end of the chain to capture both request and response data.
  • Data transformation interceptors should be placed after authentication to ensure that only authenticated requests are processed.

Advanced Interceptor Techniques§

Conditional Interceptors§

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.

Composing Interceptors§

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.

Conclusion§

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.

Quiz Time!§