Liking cljdoc? Tell your friends :D

platform

Infrastructure layer for HTTP routing/interceptors, database integration, CLI/runtime wiring, and shared platform adapters used by feature libraries.

Key namespaces

NamespacePurpose

wagoe.platform.shell.http.interceptors

HTTP interceptor composition and execution

wagoe.platform.shell.http.reitit-router

Compiles every module’s route contribution into the Ring handler

wagoe.platform.shell.adapters.database.*

Database setup, context, and shared persistence infrastructure

HTTP interceptors

Declarative cross-cutting concerns (auth, rate limiting, audit):

;; errors = wagoe.platform.core.http.errors
;; Define an interceptor
(def require-admin
  {:name  :require-admin
   :enter (fn [ctx]
            (if (admin? (get-in ctx [:request :session :user]))
              ctx
              ;; To short-circuit you MUST set :halt? true — setting :response
              ;; alone does not stop the pipeline, so the downstream handler
              ;; would run and overwrite this response. See "Interceptor phases".
              (assoc ctx :halt? true
                         :response (errors/response 403 :forbidden "Admin role required"))))
   :leave (fn [ctx] ctx)   ; optional response processing
   :error (fn [ctx err]    ; optional error handling
            (assoc ctx :response (errors/response 500 :internal-error "Internal Server Error")))})

;; Attach interceptors to routes
[["/admin"
  {:post {:handler      handlers/create-resource
          :interceptors ['auth/require-admin
                         'audit/log-action]
          :summary      "Create admin resource"}}]]

Built-in interceptors

The default HTTP stack (wagoe.platform.shell.http.interceptors/default-http-interceptors) is applied to every route unless the route opts out with :skip-interceptors? true (used only for internal endpoints such as health checks). In :enter/:leave order:

  • http-request-logging — request entry/completion logging with timing

  • http-request-metrics — timing and status-code metrics

  • http-error-reporting — captures exceptions to the error-tracking service

  • http-correlation-header — adds X-Correlation-ID to the response

  • http-csrf-protection — validates/issues CSRF tokens (see CSRF protection)

  • http-security-headers — CSP, HSTS, X-Frame-Options, X-Content-Type-Options, …

  • http-error-handler — maps an exception’s :type to an HTTP status

http-rate-limit (fixed-window, optionally Redis-backed) is available to attach per route.

:skip-interceptors? is independent of :no-doc. :no-doc only excludes a route from the Swagger spec; it does not affect interceptors. All /web routes are :no-doc yet still run the full stack so that security interceptors apply to the UI.

Error responses

Every JSON error the framework sends has one shape, whatever produced it — the exception mapping, the default-deny 401/403, CSRF, the rate limiter, a malformed body, an unknown route or method:

{"error": {"type":           "validation-error",
           "message":        "Request validation failed",
           "details":        {"email": ["invalid"]},
           "correlation-id": "5b1c9a52-6f0e-4d7a-9a57-2d9e3c1b8f40"}}

type and message are always there. details and correlation-id are there when the error has them, and dev (the BND code, dev only) sits beside them. Everything is inside error, so a client reads one key. The correlation id is also the X-Correlation-ID response header.

type is the :type of the thrown ex-info, looked up in problem-details/default-error-mappings for its status. Anything unmapped is a 500 whose type is internal-error and whose message says nothing more. Build a body of your own with wagoe.platform.core.http.errors/body or response.

The types the framework answers with:

StatusTypeMeaning

400

validation-error

The request was read, and its input refused.

400

malformed-request

The body does not parse as its Content-Type. Nothing else.

400

business-rule-violation, invalid-request

Thrown by application code; the framework maps them.

401

unauthorized, auth-failed

No valid credential.

403

forbidden, csrf-validation-failed, deletion-not-allowed, hard-deletion-not-allowed

Known caller, not allowed.

404

not-found, user-not-found, resource-not-found

Nothing there.

405

method-not-allowed

The path exists; the method does not.

409

conflict, user-exists, resource-exists

It already exists, or changed underneath.

422

unprocessable; a workflow transition’s refusal (transition-not-found, insufficient-permissions, guard-rejected, guard-not-registered)

Well-formed, and still refused.

429

rate-limit-exceeded

Too many requests.

500

internal-error

Our fault; the log has the rest.

500

missing-error-type

Dev only: an ex-info without :type reached the boundary.

501

not-supported

Not on this database or configuration.

503

unavailable

The server has no handler yet.

A write that a unique or foreign key refuses is a 409 conflict, on H2, SQLite and PostgreSQL alike: wagoe.platform.database throws it, so every generated repository gets it. details names the constraint and, when the driver names one column, the field; nothing else of the driver’s text is sent.

{"error": {"type":    "conflict",
           "message": "Another record already has this number",
           "details": {"constraint": "unique", "field": "number"}}}

A handler that knows only its status gets its type from errors/status→type: validation-error for a 4xx with no type of its own, internal-error for a 5xx.

Interceptor phases

  • :enter — request processing (auth, validation, transformation)

  • :leave — response processing (audit, metrics, transformation)

  • :error — exception handling (custom error responses)

Enter phases run in order; leave phases run in reverse order.

To reject a request from :enter, set :halt? true in the context (alongside the :response). The pipeline short-circuits only on :halt? — setting :response by itself does not stop forward execution, so a later interceptor or the route handler would overwrite it. :leave still runs for interceptors that already executed.

CSRF protection

http-csrf-protection enforces CSRF for session-authenticated, state-changing requests and issues tokens for rendering. Pure token functions live in wagoe.platform.core.csrf.

A state-changing request (POST/PUT/DELETE/PATCH) is validated — 403 on a missing or invalid token — when CSRF is enabled, the path is not exempt, and the request is either session-authenticated (session-token cookie / X-Session-Token header) or a /web route. This covers /web, /web/admin, and any session-authenticated /api route. Token-auth API clients that send no session cookie are not CSRF-vulnerable and are not checked; safe methods (GET/HEAD/OPTIONS) and :exempt-paths are skipped.

;; resources/conf/<env>/config.edn under :active
:wagoe/http
{:security
 {:csrf {:enabled?     true                                  ; OPT-IN: lib default is false
         :secret       #or [#env CSRF_SECRET #env JWT_SECRET] ; defaults to JWT_SECRET
         :exempt-paths ["/api/v1/payments/webhook"]}}}        ; trailing /* = segment-prefix

Enforcement is opt-in: the library default is :enabled? false, so upgrading the framework cannot start rejecting requests from a consumer that does not yet emit tokens. An app turns it on with the block above (after emitting tokens in its /web forms); the secret falls back to JWT_SECRET. Startup fails loud — the system wiring throws and the app refuses to boot if CSRF is enabled with a blank secret, rather than letting the interceptor fail open (run unvalidated).

Tokens are emitted with no per-handler wiring. For HTMX, either merge (csrf/hx-headers) onto an element’s attributes (e.g. <body>) so all inherited hx-* requests carry the X-CSRF-Token header, or rely on the shared page layout’s <meta name="csrf-token"> tag plus the global htmx:configRequest listener that attaches the header to every HTMX request. Plain (non-HTMX) forms include a hidden field:

(require '[wagoe.platform.core.csrf :as csrf])

[:form {:method "post" :action "/web/logout"}
 (csrf/hidden-field)        ; reads the token bound for the current request
 [:button "Logout"]]

See the authentication guide for the request flow and the unauthenticated (login) pre-session token.

Testing

clojure -M:test:test/pg :platform

Can you improve this documentation? These fine people already did:
Thijs Creemers & thijscreemers
Edit on GitHub

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
←Move to previous article
→Move to next article
Ctrl+/Jump to the search field
× close