Small, focused Ring middleware for security and observability.
This library collects a set of middleware components used across Sturdy Statistics web services. Each middleware does one thing, is explicit, and is intended to be composed deliberately rather than enabled wholesale.
This library reflects the needs and opinions of Sturdy Statistics. We may not accept feature requests that dilute its focus.
These are not intended to be framework defaults; rather, they encode specific operational and security decisions that we want to apply consistently across our applications.
Note that sturdy-middleware assumes the ingress layer (Nginx/Cloudflare) is responsible for sanitizing proxy headers.
sturdy-middleware supports synchronous Ring handlers only.
Its middleware wrappers implement the one-argument (handler request) contract and do not support Ring's asynchronous (handler request respond raise) contract.
Explicit code Middleware should make policy obvious at the call site.
Idempotent and safe Middleware should be safe to apply once or multiple times without surprising behavior.
Fail closed where appropriate Deny on missing or ambiguous input, especially for auth-related or state-changing endpoints.
Cheap by default No unnecessary allocation, parsing, or reflection on hot paths.
Add to deps.edn:
{:deps {com.sturdystats/sturdy-middleware {:mvn/version "VERSION"}}}
| Middleware | Purpose | Typical scope |
|---|---|---|
wrap-request-id | Stable request correlation | All requests |
wrap-max-request-size | Reject oversized uploads early | Upload / API endpoints |
wrap-require-same-origin | CSRF protection (tolerant) | Authenticated endpoints |
wrap-require-same-origin-strict | CSRF protection (strict) | Auth endpoints |
wrap-nostore | Prevent caching of private responses | Logged-in pages, dashboards |
wrap-nostore-on-errors | Prevent caching of error responses | All requests |
with-vary-cookie | Prevent cache mixing across users | Logged-in pages |
with-noindex | Prevent indexing by search engines | Private or internal pages |
wrap-request-idAttach a request identifier to every request and response.
X-Request-Id, X-Correlation-Id, traceparent):request-id in the Ring requestX-Request-Id)(wrap-request-id handler)
wrap-max-request-sizeReject requests with a Content-Length exceeding a configured limit.
Content-Length and chunked transfer encoding with HTTP 411 Length Required(wrap-max-request-size handler (* 10 1024 1024)) ; 10 MB
wrap-max-request-size infers that a request has a body from its Content-Length or Transfer-Encoding header.
If both headers are absent, the middleware passes the request through without limiting its :body stream.
This can affect HTTP/2 requests because HTTP/2 frames request bodies without using Transfer-Encoding: chunked, and a Ring adapter may expose such a body without a Content-Length header.
Applications must not rely on this middleware as the only body-size boundary for headerless requests.
Enforce a protocol-level request-size limit at the ingress or Ring adapter as well.
A future version may add protocol-independent stream limiting or an explicit policy requiring Content-Length on selected request methods.
wrap-require-same-origin (tolerant)Origin header(wrap-require-same-origin handler)
wrap-require-same-origin-strictOrigin(wrap-require-same-origin-strict handler)
wrap-nostorePrevents caching of user-specific or sensitive responses.
(wrap-nostore handler)
wrap-nostore-on-errorsApplies no-store headers to error responses and mutable requests.
(wrap-nostore-on-errors handler)
with-vary-cookieEnsures responses vary on Cookie without duplication.
(with-vary-cookie response)
with-noindexPrevents search engines from indexing private pages.
(with-noindex response)
(-> handler
(#(wrap-max-request-size % (* 10 1024 1024)))
wrap-require-same-origin
wrap-nostore-on-errors
wrap-nostore
wrap-request-id)
(-> handler
wrap-require-same-origin-strict
wrap-nostore
wrap-request-id)
This library exists to centralize security-affecting middleware so that:
Designed for production use in long-running services.
wrap-max-request-size exposes a dynamic rendering hook that applications may rebind to integrate with their own error views.
By default, oversized requests return:
When a moderately oversized request body is drained successfully, the connection remains reusable.
If the declared body is too large to drain or the drain cannot complete, the response includes Connection: close.
This default is safe and dependency-free, but is likely too basic for production use. If you use this library, you will want to re-bind it.
Applications may rebind *render-too-large* to customize either the response body or entire response.
Because the hook is read when a request is handled, any dynamic binding must wrap request handling, not middleware construction.
The hook is called with a context map containing:
{:code 413
:title "Content too large"
:blurb "Upload failed."
:message "... human-readable explanation ..."
:request-id "abc123" ; if present
:request <ring-request-map>
:max-upload-bytes 10485760}
The function may return either:
(require
'[sturdy.middleware.request-size :as rs]
'[ring-errors.views :as err-v])
(def render-too-large
(fn [{:keys [message request-id]}]
(err-v/error-page
{:code 413
:title "Content too large"
:blurb "Upload failed."}
{:message message
:id request-id})))
(def app
(let [wrapped (rs/wrap-max-request-size handler (* 10 1024 1024))]
(fn [request]
(binding [rs/*render-too-large* render-too-large]
(wrapped request)))))
If you prefer a single global configuration:
(alter-var-root
#'rs/*render-too-large*
(constantly
(fn [ctx]
(my-custom-413-view ctx))))
The render hook can inspect request headers (e.g. Accept) and return JSON for API clients while keeping HTML for browsers.
(fn [{:keys [request message]}]
(if (= "application/json"
(get-in request [:headers "accept"]))
{:status 413
:headers {"Content-Type" "application/json"}
:body {:error "payload-too-large"
:message message}}
(html-view message)))
Apache License 2.0
Copyright © Sturdy Statistics
Can you improve this documentation?Edit on GitHub
cljdoc 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 |