HTTP interceptor pipeline for Ring request/response processing.
This namespace provides an HTTP-specific interceptor chain that runs over Ring handlers, giving Pedestal-like enter/leave/error semantics while maintaining Ring compatibility.
Key Benefits:
HTTP Context Model: {:request Ring request map :response Ring response map (built up through pipeline) :route Route metadata from Reitit match :path-params Extracted path parameters :query-params Extracted query parameters :system Observability services {:logger :metrics-emitter :error-reporter} :attrs Additional attributes set by interceptors :correlation-id Unique request ID :started-at Request start timestamp}
Interceptor Shape: {:name :my-interceptor :enter (fn [context] ...) ; Process request, modify context :leave (fn [context] ...) ; Process response, modify context :error (fn [context] ...)} ; Handle exceptions, produce safe response
Usage: ;; As Ring middleware (global) (def handler (-> app-handler (wrap-http-interceptors [logging metrics error-reporting])))
;; Per-route via Reitit :middleware {:get {:handler my-handler :middleware [(interceptor-middleware [auth rate-limit])]}}
Integration with Normalized Routes:
HTTP interceptor pipeline for Ring request/response processing.
This namespace provides an HTTP-specific interceptor chain that runs over Ring handlers,
giving Pedestal-like enter/leave/error semantics while maintaining Ring compatibility.
Key Benefits:
- Consistent cross-layer observability (HTTP → service → persistence)
- Declarative per-route policies (auth, rate-limit, auditing)
- Clean separation of concerns (routing vs cross-cutting logic)
- Reusable interceptor components across routes
HTTP Context Model:
{:request Ring request map
:response Ring response map (built up through pipeline)
:route Route metadata from Reitit match
:path-params Extracted path parameters
:query-params Extracted query parameters
:system Observability services {:logger :metrics-emitter :error-reporter}
:attrs Additional attributes set by interceptors
:correlation-id Unique request ID
:started-at Request start timestamp}
Interceptor Shape:
{:name :my-interceptor
:enter (fn [context] ...) ; Process request, modify context
:leave (fn [context] ...) ; Process response, modify context
:error (fn [context] ...)} ; Handle exceptions, produce safe response
Usage:
;; As Ring middleware (global)
(def handler
(-> app-handler
(wrap-http-interceptors [logging metrics error-reporting])))
;; Per-route via Reitit :middleware
{:get {:handler my-handler
:middleware [(interceptor-middleware [auth rate-limit])]}}
Integration with Normalized Routes:
- Normalized routes can specify :interceptors vector
- Reitit adapter translates :interceptors → :middleware with this runner(create-global-http-middleware system)(create-global-http-middleware system additional-interceptors)Creates global HTTP middleware with default observability stack.
Args: system: Map containing observability services additional-interceptors: Optional vector of additional interceptors to prepend
Returns: Ring middleware function
Creates global HTTP middleware with default observability stack. Args: system: Map containing observability services additional-interceptors: Optional vector of additional interceptors to prepend Returns: Ring middleware function
(create-http-context request system & [route-data])Creates an HTTP interceptor context from a Ring request.
Args: request: Ring request map system: Map containing observability services {:logger :metrics-emitter :error-reporter} route-data: Optional Reitit route match data
Returns: HTTP context map with request details and observability services
Creates an HTTP interceptor context from a Ring request.
Args:
request: Ring request map
system: Map containing observability services {:logger :metrics-emitter :error-reporter}
route-data: Optional Reitit route match data
Returns:
HTTP context map with request details and observability servicesDefault HTTP interceptor stack for observability and error handling.
Default HTTP interceptor stack for observability and error handling.
Default security headers for production deployment.
Provides defense-in-depth protection against common web vulnerabilities:
Note: Uses Alpine.js CSP build (@alpinejs/csp) which avoids eval()/new Function(). All Alpine components are registered via Alpine.data() in components.js / admin-ux.js. 'unsafe-inline' is kept in script-src because several UI components still use inline event handlers (onclick, onchange, onsubmit) and inline [:script] blocks (e.g. Alpine store init, audit modal wiring). Remove 'unsafe-inline' only after those are externalised. 'unsafe-eval' is kept in script-src because HTMX 2.x has allowEval:true by default and hx-on::* expression attrs (e.g. hx-on::afterRequest in admin entity forms) depend on it. Remove 'unsafe-eval' only after htmx.config.allowEval is disabled or all hx-on attrs are replaced.
Default security headers for production deployment. Provides defense-in-depth protection against common web vulnerabilities: - Content-Security-Policy: Prevent XSS attacks - X-Frame-Options: Prevent clickjacking - Strict-Transport-Security: Force HTTPS (with preload) - X-Content-Type-Options: Prevent MIME sniffing - Cross-Origin-Opener-Policy: Isolate top-level browsing context - Referrer-Policy: Control referrer information Note: Uses Alpine.js CSP build (@alpinejs/csp) which avoids eval()/new Function(). All Alpine components are registered via Alpine.data() in components.js / admin-ux.js. 'unsafe-inline' is kept in script-src because several UI components still use inline event handlers (onclick, onchange, onsubmit) and inline [:script] blocks (e.g. Alpine store init, audit modal wiring). Remove 'unsafe-inline' only after those are externalised. 'unsafe-eval' is kept in script-src because HTMX 2.x has allowEval:true by default and hx-on::* expression attrs (e.g. hx-on::afterRequest in admin entity forms) depend on it. Remove 'unsafe-eval' only after htmx.config.allowEval is disabled or all hx-on attrs are replaced.
(extract-response context)Extracts the Ring response from an HTTP context.
If :response exists in context, returns it. If :response is nil, returns a safe 500 error response.
Args: context: HTTP interceptor context
Returns: Ring response map
Extracts the Ring response from an HTTP context. If :response exists in context, returns it. If :response is nil, returns a safe 500 error response. Args: context: HTTP interceptor context Returns: Ring response map
(get-client-id request)Extracts client identifier from request for rate limiting.
Priority order:
Args: request: Ring request map
Returns: String client identifier
Extracts client identifier from request for rate limiting. Priority order: 1. Authenticated user ID (from :user in request) 2. API key (from headers) 3. Remote IP address Args: request: Ring request map Returns: String client identifier
Adds correlation ID to response headers.
Adds correlation ID to response headers.
Validates CSRF tokens for state-changing requests (POST, PUT, DELETE, PATCH) and issues tokens for rendering.
Binding model — a token is signed against, and validated with, a per-client binding so a forged cross-site request cannot produce a matching token:
A state-changing request is validated (403 on failure) when CSRF is enabled, the path is not in :exempt-paths, and it is either session-authenticated or a /web route. This protects /web/admin and any session-authenticated /api route; it does NOT check token-auth API clients that send no session cookie (not CSRF-vulnerable) or exempt paths (webhooks/callbacks). Safe methods (GET/HEAD/OPTIONS) are never validated.
For rendering, the interceptor exposes a token on the request as :anti-forgery-token and binds csrf/token around handler execution, so the page
<meta> tag (HTMX) and form hidden fields emit it without per-handler threading. When the request already carries a token valid for the current binding, that token is re-issued instead of minting a new one (tokens have no expiry, so this is equivalent); a fresh token is minted only when absent or invalid.
Config is read from (:csrf system), injected by the HTTP handler wiring: {:enabled? bool, :secret <signing-key>, :exempt-paths ["/api/v1/..."]} Enforcement is opt-in: when :enabled? is absent or false the interceptor is a no-op (no validation, no issuance). Apps enable it after emitting tokens.
Validates CSRF tokens for state-changing requests (POST, PUT, DELETE, PATCH) and
issues tokens for rendering.
Binding model — a token is signed against, and validated with, a per-client
binding so a forged cross-site request cannot produce a matching token:
- Authenticated requests bind to the session (session-token cookie / header).
- Unauthenticated /web flows (login, register, MFA) bind to a pre-session cookie
(csrf-session, SameSite=Strict) minted on the page GET.
A state-changing request is validated (403 on failure) when CSRF is enabled, the
path is not in :exempt-paths, and it is either session-authenticated or a /web
route. This protects /web/admin and any session-authenticated /api route; it does
NOT check token-auth API clients that send no session cookie (not CSRF-vulnerable)
or exempt paths (webhooks/callbacks). Safe methods (GET/HEAD/OPTIONS) are never
validated.
For rendering, the interceptor exposes a token on the request as
:anti-forgery-token and binds csrf/*token* around handler execution, so the page
<meta> tag (HTMX) and form hidden fields emit it without per-handler threading.
When the request already carries a token valid for the current binding, that
token is re-issued instead of minting a new one (tokens have no expiry, so this
is equivalent); a fresh token is minted only when absent or invalid.
Config is read from (:csrf system), injected by the HTTP handler wiring:
{:enabled? bool, :secret <signing-key>, :exempt-paths ["/api/v1/..."]}
Enforcement is opt-in: when :enabled? is absent or false the interceptor is a
no-op (no validation, no issuance). Apps enable it after emitting tokens.Converts exceptions into safe HTTP error responses.
Converts exceptions into safe HTTP error responses.
Captures HTTP exceptions and reports them to error tracking.
Captures HTTP exceptions and reports them to error tracking.
Default latency histogram buckets in SECONDS (Prometheus/OTel convention).
Default latency histogram buckets in SECONDS (Prometheus/OTel convention).
(http-rate-limit)(http-rate-limit limit window-ms)(http-rate-limit limit window-ms cache)Creates a rate limiting interceptor with fixed limit/window/cache.
With a cache argument, uses Redis fixed-window counting — safe for multi-instance deployments and survives process restarts. Without a cache argument, falls back to an in-process sliding window.
For the framework default pipeline use the config-driven
http-rate-limit-protection instead; this fixed-arg form is for explicit
per-route limits and standalone wiring (e.g. the devtools dashboard).
Args: limit: Maximum requests per time window (default: 100) window-ms: Time window in milliseconds (default: 60000 = 1 minute) cache: (optional) IAtomicCache + ICache implementation for distributed limiting
Returns: Rate limiting interceptor map
Example: (http-rate-limit) ; 100 req/min, in-memory (http-rate-limit 30 60000) ; 30 req/min, in-memory (http-rate-limit 100 60000 cache) ; 100 req/min, Redis-backed
Creates a rate limiting interceptor with fixed limit/window/cache. With a cache argument, uses Redis fixed-window counting — safe for multi-instance deployments and survives process restarts. Without a cache argument, falls back to an in-process sliding window. For the framework default pipeline use the config-driven `http-rate-limit-protection` instead; this fixed-arg form is for explicit per-route limits and standalone wiring (e.g. the devtools dashboard). Args: limit: Maximum requests per time window (default: 100) window-ms: Time window in milliseconds (default: 60000 = 1 minute) cache: (optional) IAtomicCache + ICache implementation for distributed limiting Returns: Rate limiting interceptor map Example: (http-rate-limit) ; 100 req/min, in-memory (http-rate-limit 30 60000) ; 30 req/min, in-memory (http-rate-limit 100 60000 cache) ; 100 req/min, Redis-backed
Config-driven rate limiter applied in the default HTTP interceptor stack.
Reads its policy from the system map injected by the HTTP handler wiring: (:rate-limit system) => {:enabled? bool, :limit int, :window-ms int} (:cache system) => IAtomicCache (Redis) for cross-replica limiting
Enforcement is opt-in: when :enabled? is absent or false the interceptor is a no-op, so a framework upgrade cannot start 429-ing consumers that have not configured limits. When enabled it uses the Redis cache for a limit shared across all replicas; with no cache it falls back to a per-process counter — correct on a single node only, NOT a global limit across replicas.
Config-driven rate limiter applied in the default HTTP interceptor stack.
Reads its policy from the system map injected by the HTTP handler wiring:
(:rate-limit system) => {:enabled? bool, :limit int, :window-ms int}
(:cache system) => IAtomicCache (Redis) for cross-replica limiting
Enforcement is opt-in: when :enabled? is absent or false the interceptor is a
no-op, so a framework upgrade cannot start 429-ing consumers that have not
configured limits. When enabled it uses the Redis cache for a limit shared
across all replicas; with no cache it falls back to a per-process counter —
correct on a single node only, NOT a global limit across replicas.Logs HTTP requests at entry and completion.
Logs HTTP requests at entry and completion.
Records HTTP request count, error count, and latency (seconds) on the metrics
emitter. http.requests is incremented for every response (success and
error, with a status label); http.requests.errors additionally counts the
error path. Handles are registered once at wiring and threaded in as
:metrics-handles on the system map; a no-op when no metrics component is
configured, so this is safe always-on.
Records HTTP request count, error count, and latency (seconds) on the metrics emitter. `http.requests` is incremented for every response (success *and* error, with a `status` label); `http.requests.errors` additionally counts the error path. Handles are registered once at wiring and threaded in as `:metrics-handles` on the system map; a no-op when no metrics component is configured, so this is safe always-on.
Starts a distributed-tracing span per HTTP request and ends it on completion,
recording the response status (and any exception). No-op unless a tracer is
wired into :system (:no-op tracer by default), so this is safe always-on.
The span is a per-request root: cross-thread propagation into service /
persistence spans is a later enhancement (would need context support on the
ITracer port).
Starts a distributed-tracing span per HTTP request and ends it on completion, recording the response status (and any exception). No-op unless a tracer is wired into `:system` (:no-op tracer by default), so this is safe always-on. The span is a per-request root: cross-thread propagation into service / persistence spans is a later enhancement (would need context support on the `ITracer` port).
(http-security-headers)(http-security-headers headers)Creates security headers interceptor with configurable headers.
Args: headers - Optional map of security headers (defaults to default-security-headers)
Returns: Interceptor that adds security headers to responses
Examples: ;; Use default security headers (http-security-headers)
;; Custom CSP for development (allows unsafe-eval for hot reload) (http-security-headers {"Content-Security-Policy" "default-src 'self' 'unsafe-eval'; ..." "X-Frame-Options" "SAMEORIGIN"})
;; Disable HSTS in development (http-security-headers (dissoc default-security-headers "Strict-Transport-Security"))
Security Headers Explained:
Content-Security-Policy (CSP): Defines approved sources for loading content (scripts, styles, images, etc.) Prevents XSS attacks by restricting where resources can be loaded from Current policy:
X-Frame-Options: DENY Legacy header (superseded by CSP frame-ancestors) Prevents page from being embedded in iframe/frame Protects against clickjacking attacks
Strict-Transport-Security (HSTS): Forces browsers to use HTTPS for 1 year Prevents protocol downgrade attacks includeSubDomains applies to all subdomains
X-Content-Type-Options: nosniff Prevents browser from MIME-sniffing responses Forces browser to respect Content-Type header
X-XSS-Protection: 1; mode=block Legacy XSS filter (modern browsers use CSP instead) Tells browser to block page if XSS attack detected
Referrer-Policy: strict-origin-when-cross-origin Controls Referer header sent with requests Only sends origin (not full URL) on cross-origin requests
Permissions-Policy: Controls browser features (geolocation, camera, microphone) Denies access to sensitive features by default
Environment-Specific Recommendations:
Development:
Staging:
Production:
Creates security headers interceptor with configurable headers.
Args:
headers - Optional map of security headers (defaults to default-security-headers)
Returns:
Interceptor that adds security headers to responses
Examples:
;; Use default security headers
(http-security-headers)
;; Custom CSP for development (allows unsafe-eval for hot reload)
(http-security-headers
{"Content-Security-Policy" "default-src 'self' 'unsafe-eval'; ..."
"X-Frame-Options" "SAMEORIGIN"})
;; Disable HSTS in development
(http-security-headers
(dissoc default-security-headers "Strict-Transport-Security"))
Security Headers Explained:
- Content-Security-Policy (CSP):
Defines approved sources for loading content (scripts, styles, images, etc.)
Prevents XSS attacks by restricting where resources can be loaded from
Current policy:
* default-src 'self' - Only load resources from same origin by default
* script-src - Allow scripts from self + HTMX from CDN
* style-src - Allow styles from self + Pico CSS from CDN
* img-src - Allow images from self, data URLs, and HTTPS
* frame-ancestors 'none' - Prevent embedding in iframes (clickjacking)
- X-Frame-Options: DENY
Legacy header (superseded by CSP frame-ancestors)
Prevents page from being embedded in iframe/frame
Protects against clickjacking attacks
- Strict-Transport-Security (HSTS):
Forces browsers to use HTTPS for 1 year
Prevents protocol downgrade attacks
includeSubDomains applies to all subdomains
- X-Content-Type-Options: nosniff
Prevents browser from MIME-sniffing responses
Forces browser to respect Content-Type header
- X-XSS-Protection: 1; mode=block
Legacy XSS filter (modern browsers use CSP instead)
Tells browser to block page if XSS attack detected
- Referrer-Policy: strict-origin-when-cross-origin
Controls Referer header sent with requests
Only sends origin (not full URL) on cross-origin requests
- Permissions-Policy:
Controls browser features (geolocation, camera, microphone)
Denies access to sensitive features by default
Environment-Specific Recommendations:
Development:
- Relax CSP to allow hot reload: 'unsafe-eval', 'unsafe-inline'
- Use X-Frame-Options: SAMEORIGIN (for development tools)
- Omit HSTS (HTTP development servers)
Staging:
- Stricter CSP (fewer 'unsafe-*' directives)
- Include HSTS with shorter max-age (e.g., 86400 = 1 day)
Production:
- Strictest CSP (no 'unsafe-*' if possible)
- HSTS with max-age=31536000 (1 year)
- Consider adding preload directive for HSTS(interceptor-middleware interceptors system)Creates a Ring middleware function from interceptors.
This is useful for Reitit per-route :middleware.
Usage: {:get {:handler my-handler :middleware [(interceptor-middleware [auth rate-limit] system)]}}
Args: interceptors: Vector of interceptor maps system: Map containing observability services
Returns: Ring middleware function (handler → wrapped-handler)
Creates a Ring middleware function from interceptors.
This is useful for Reitit per-route :middleware.
Usage:
{:get {:handler my-handler
:middleware [(interceptor-middleware [auth rate-limit] system)]}}
Args:
interceptors: Vector of interceptor maps
system: Map containing observability services
Returns:
Ring middleware function (handler → wrapped-handler)(merge-response-headers context headers)Merges additional headers into the response.
Args: context: HTTP interceptor context headers: Map of headers to merge
Returns: Updated context with merged headers
Merges additional headers into the response. Args: context: HTTP interceptor context headers: Map of headers to merge Returns: Updated context with merged headers
(register-http-metrics! metrics)Register the standard HTTP request metrics on a metrics component once (at
wiring time). Returns a handles map {:requests :errors :duration}, or nil
when there is no metrics component. Registering once — rather than per request
— is required: some adapters (e.g. datadog) reset a counter on re-register.
Register the standard HTTP request metrics on a metrics component once (at
wiring time). Returns a handles map `{:requests :errors :duration}`, or nil
when there is no metrics component. Registering once — rather than per request
— is required: some adapters (e.g. datadog) reset a counter on re-register.(run-http-interceptors handler interceptors request system)Runs an HTTP interceptor pipeline around a Ring handler.
Args: handler: Ring handler function (request → response) interceptors: Vector of interceptor maps request: Ring request map system: Map containing observability services
Returns: Ring response map
Pipeline execution:
Runs an HTTP interceptor pipeline around a Ring handler. Args: handler: Ring handler function (request → response) interceptors: Vector of interceptor maps request: Ring request map system: Map containing observability services Returns: Ring response map Pipeline execution: 1. Create HTTP context from request 2. Run :enter phase on all interceptors 3. If successful, call handler and set :response in context 4. Run :leave phase on all interceptors (reverse order) 5. If exception occurs, run :error phase to produce safe response 6. Extract and return final Ring response
(set-response context response)Sets the Ring response in the HTTP context.
Args: context: HTTP interceptor context response: Ring response map
Returns: Updated context with response
Sets the Ring response in the HTTP context. Args: context: HTTP interceptor context response: Ring response map Returns: Updated context with response
(update-response-body context f)Updates the response body using a function.
Args: context: HTTP interceptor context f: Function to apply to existing body
Returns: Updated context with modified body
Updates the response body using a function. Args: context: HTTP interceptor context f: Function to apply to existing body Returns: Updated context with modified body
(validate-http-interceptor interceptor)Validates that an interceptor is suitable for HTTP use.
HTTP interceptors should handle HTTP context properly and produce valid Ring responses.
Validates that an interceptor is suitable for HTTP use. HTTP interceptors should handle HTTP context properly and produce valid Ring responses.
(wrap-csrf handler {:keys [enabled? secret exempt-paths] :or {enabled? false}})Ring-middleware form of http-csrf-protection, for handlers that run OUTSIDE
the interceptor stack — e.g. an app that mounts its own routes in front of the
platform handler and so never passes through the default interceptor chain.
Identical binding model and semantics to the interceptor:
csrf/*token* around
the handler, so form hidden-fields and the page <meta> tag emit it without
per-handler threading.csrf-session cookie on the response (for login/register/MFA forms).Opt-in: a falsy/absent :enabled? or a blank :secret makes it a pass-through.
Config: {:enabled? bool :secret <signing-key> :exempt-paths ["/..."]}.
Ring-middleware form of `http-csrf-protection`, for handlers that run OUTSIDE
the interceptor stack — e.g. an app that mounts its own routes in front of the
platform handler and so never passes through the default interceptor chain.
Identical binding model and semantics to the interceptor:
- State-changing (POST/PUT/DELETE/PATCH) requests that are not exempt and are
either session-authenticated or a /web route are validated against the
session token / pre-session cookie binding; a bad or absent token yields 403
and the wrapped handler does not run.
- Safe or authenticated requests issue a token and bind `csrf/*token*` around
the handler, so form hidden-fields and the page `<meta>` tag emit it without
per-handler threading.
- An unauthenticated /web page load mints a pre-session binding and sets the
`csrf-session` cookie on the response (for login/register/MFA forms).
- Safe methods (GET/HEAD/OPTIONS) and token-auth API clients (no session
cookie, not CSRF-vulnerable) are never blocked.
Opt-in: a falsy/absent `:enabled?` or a blank `:secret` makes it a pass-through.
Config: {:enabled? bool :secret <signing-key> :exempt-paths ["/..."]}.(wrap-http-interceptors handler interceptors system)Ring middleware that runs HTTP interceptors around a handler.
Usage: (def handler (-> my-handler (wrap-http-interceptors [auth logging metrics] system)))
Args: handler: Ring handler function interceptors: Vector of interceptor maps system: Map containing observability services
Returns: Wrapped Ring handler function
Ring middleware that runs HTTP interceptors around a handler.
Usage:
(def handler
(-> my-handler
(wrap-http-interceptors [auth logging metrics] system)))
Args:
handler: Ring handler function
interceptors: Vector of interceptor maps
system: Map containing observability services
Returns:
Wrapped Ring handler functioncljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |