Middleware
Wrap handlers with focused request and response behavior.
What a wrapper does
Middleware is a function taking a handler and returning a handler. Request transformations run before the inner handler; response transformations run after its result. Put wrappers around the Ring handler before adapting it with ft-handler, or pass the wrapped Ring handler to server/listen!.
(ns application.forms
(:require [fast-twitch.routing :as routing]
[fast-twitch.middlewares.params :as params]
[fast-twitch.middlewares.nested-params :as nested]
[fast-twitch.middlewares.keyword-params :as keywords]))
(defn echo-fields [request]
{:status 200 :headers {"content-type" "text/plain; charset=utf-8"}
:body (pr-str (:params request))})
(def app
(-> (routing/routes [{:pattern "/form" :method :post :handler echo-fields}]
(fn [_] (routing/not-found "Missing")))
(keywords/wrap-keyword-params)
(nested/wrap-nested-params)
(params/wrap-params)))
This is the Ring application portion; use the startup guide or complete forms project to run it. A POST body user[name]=Ada becomes a nested keyword-keyed :params map. The last wrapper written, params, runs first: decoding → nesting → keyword conversion → routing → handler. Response processing unwinds in the opposite order. Changing the order can leave a handler with flat or undecoded parameters.
The shared common wrapper helpers support native Responses, response maps, Promises and callback handlers. A transform can be asynchronous. A three-argument callback handler must deliver through respond / raise; returning a value is not delivery.
Choose API or site defaults
(ns application.defaults
(:require [fast-twitch.middlewares.defaults :as defaults]))
(defn handler [_]
{:status 200 :headers {"content-type" "text/plain"} :body "Hello"})
(def api (defaults/wrap-defaults handler defaults/api-defaults))
(def site (defaults/wrap-defaults handler defaults/site-defaults))
wrap-defaults takes [handler options]; choose one of these application handlers for startup. The map is configuration, not a list of wrapper functions. False disables a configurable feature; enabled features accepting options use their own map. The source/reference has the full map and composition order.
| Configuration path | API default | Site default |
|---|---|---|
[:params :urlencoded], [:params :keywordize] |
true | true |
[:params :multipart], [:params :nested] |
Absent | true |
[:params :method-override] |
false | false |
:cookies |
Absent | true |
:session |
Absent | Cookie name ft-session, HttpOnly, SameSite lax, flash enabled |
[:security :anti-forgery] |
Absent | Enabled with :safe-header "X-Ft-Anti-Forgery" |
| Request ID, size limit, rate limit, timeout | false | false |
| Not-modified, content length, content types | true | true |
| Default charset | "utf-8" |
"utf-8" |
| Cache policy, absolute redirects, logging | false | false |
| CSP, SSL redirect, HSTS, proxy headers | false | false |
| X headers | nosniff, sameorigin, X-XSS-Protection "0" |
Same |
HEAD wrapping is always included by the current defaults response chain. JSON body decoding is not supplied by wrap-params or either defaults map: use a configured JSON listener or a dedicated bounded body reader. Site defaults are a starting configuration, not automatically persistent sessions or a trust policy for every deployment.
Parameters and uploads
| Wrapper/API | Request data and settings |
|---|---|
| params | Query/URL-encoded parsing into string-keyed :query-params, :form-params, merged :params; the current :encoding option is passed through but ignored by the parser |
| multipart-params | Native FormData parsing into :multipart-params and :params; repeated fields become vectors; current parser does not enforce a supplied upload byte-limit option |
| nested-params | Bracketed field names become nested maps/vectors after parsing |
| keyword-params | Converts supported parsed parameter keys to keywords recursively; values are not coerced |
| method-override | A POST can select an allowed alternate method through its configured parameter/header |
An upload field is {:filename ... :content-type ... :size ... :file native-file :tempfile native-file}. The native File is not a disk pathname. The upload project shows the wrapper order and Hiccup form; it reports metadata without storing the file. Decoding a body consumes it; downstream code should use parsed data rather than reading the raw body again.
Cookies, sessions and flash
(ns application.sessions
(:require [fast-twitch.middlewares.session :as session]))
(def store (session/memory-store))
(def app
(session/wrap-session
(fn [request]
(let [visits (inc (get-in request [:session :visits] 0))]
{:status 200 :headers {"content-type" "text/plain"}
:body (str "Visit " visits)
:session (assoc (:session request) :visits visits)}))
{:store store :cookie-name "visits"
:cookie-attrs {:http-only true :same-site :lax}}))
Use the same cookie jar across requests to observe increasing visits. :store is an atom-backed map in this implementation, not an arbitrary database interface. :cookie-name defaults to "ft-session"; :root defaults to /; :cookie-attrs and per-response :session-cookie-attrs merge with default path and HttpOnly settings. Set Secure for HTTPS cookies where appropriate.
Omitting response :session leaves stored data unchanged. A response :session map replaces saved session data, so merge the request's session when preserving fields. Explicit :session nil deletes the session and emits an expiring cookie. Cookie values are separate from the session's contents. Default/injected memory stores belong to a process and are not shared across replicas or durable across restart.
Cookies middleware reads cookie headers into :cookies and serializes response :cookies into separate Set-Cookie entries. A cookie instruction contains a value and attributes such as path, max-age, secure, http-only and same-site. Flash middleware moves one-time data through the session: wrap it inside session loading/saving. Complete authenticated todo project.
Request controls and observability
| Wrapper | Settings / default behavior |
|---|---|
| request-id | Request ID generation/propagation; inspect its configurable header and ID function in the reference |
| request-size-limit | :max-bytes or numeric option; declared Content-Length only; default 413 error response; optional :error-response / :error-handler |
| rate-limit | :requests-per-second / :rate default 10, :burst defaults to ceiling(rate), :store, :key-fn; default 429 with Retry-After 1; custom error hook receives request and result |
| timeout | :timeout-ms / :ms default 30000; default 503; optional :error-response / :error-handler |
| logging | Optional :logger and :event-fn; completion event rather than automatic stream-by-stream logging |
A Content-Length check does not bound an undeclared/chunked body. Use actual bounded readers for that. Rate buckets default to an in-memory store per wrapped handler; multiple processes do not form one global limit. The default key uses real-ip, remote-addr or unknown, so all users share a key if the application supplies no useful identity.
Timeout bounds completion of a Promise/callback response and disposes a late losing response body. It does not preempt synchronous JavaScript, cancel arbitrary provider work or limit the lifetime of an already-returned SSE body. Connect upstream cancellation/deadlines in business logic.
Response, file and deployment wrappers
| API | Purpose and important constraint |
|---|---|
| common | Header helpers, Fetch normalization, Promise-aware request/response transforms |
| content-type | MIME inference and file sniffing; :mime-types overrides extension mappings |
| default-charset | Adds a fallback charset to applicable text media types; wrapper accepts the charset string |
| content-length | Measures supported bodies, including UTF-8 byte length; does not consume streams to measure them |
| head | Executes GET behavior for HEAD and removes/disposes the body |
| not-modified | Uses ETag/Last-Modified request validators to return bodyless 304 where applicable |
| absolute-redirects | Rewrites redirect Location headers using current request URL information |
| cache-policy | Header-policy memoization, not response-body caching; static-prefix policy and explicit dynamic policy |
| file | Filesystem root and URL root, path checks, index/dotfile/header settings |
| resource | Delegates resource serving to the filesystem file wrapper; not a JVM classpath reader |
| file-info | File metadata/header enrichment around responses |
| proxy-headers | Forwarded host/scheme/IP adaptation and :real-ip-headers; establish proxy trust outside this wrapper |
| ssl | SSL redirect/HSTS through :ssl-redirect?, :hsts?, nested :hsts settings |
| content-security-policy | Structured CSP config, serialization/validation and optional header-name override |
| x-headers | :content-type-options, :frame-options, :xss-protection response headers |
For caching, :max-age defaults to 31536000 and immutable is enabled unless false for matching static prefixes; only apply that long policy to versioned assets. Dynamic responses default to no-cache/no-store. Cache settings include :static-prefixes, :static-policy, :dynamic-policy, :cacheable?, :etag-fn, :key-fn, :backend, :max-entries; backends memoize policy maps.
HSTS options include max-age (default 31536000), include-subdomains? and preload?. Configure forwarded headers before components that use rewritten scheme/host. Header interpretation alone does not authenticate an untrusted proxy. File handling details.
Anti-forgery tokens and async rendering
The anti-forgery middleware puts the token on the request as :anti-forgery-token, stores it in the outgoing session, and binds *anti-forgery-token* and *anti-forgery-param-name* while invoking the handler. By default, submitted tokens are read from the __anti-forgery-token form parameter or the X-CSRF-Token / X-XSRF-Token headers.
Dynamic bindings do not automatically survive an await or a later JavaScript callback. Capture request values before awaiting, or use bound-anti-forgery-fn to capture the current bindings for a renderer:
(ns application.forms
(:require [cljs.core :refer [await]]
[fast-twitch.middlewares.anti-forgery :as anti-forgery]))
(defn ^:async render-after [request load-data render-form]
(let [render (anti-forgery/bound-anti-forgery-fn render-form)]
(render (await (load-data request)))))
Call render-after from the protected handler. load-data supplies the form's data; render-form synchronously renders the response with the captured token and parameter name. If the renderer also awaits, capture the values at its entry before that await. Compose session and parameter parsing outside anti-forgery so it receives the decoded session and submitted token, or use wrap-defaults with site-defaults.
Custom anti-forgery :read-token receives [request options]; :error-handler receives [request failure]. These hook arities also apply to the three-argument Ring callback path. Returning :session nil clears the session instead of restoring its anti-forgery token. The built-in session store is process-local; choose its lifetime with your application's deployment in mind.
Streaming caveats
The timeout middleware bounds response creation, not the stream lifetime, and disposes a late losing response body. Add an explicit upstream deadline and disconnect cancellation. Content-length now measures UTF-8 bytes for strings and sequential bodies, and byte lengths for buffers, typed views and Blobs; native streams remain unmeasured. HEAD and conditional-response wrappers remove the body and dispose the discarded stream. Check each wrapper's supported body types before adding it to a streaming handler.