re-frame.http¶
Managed HTTP is an optional capability. An event dispatches the [:rf.http/managed args-map] fx. The implementation owns retries, cancellation, timeouts, decode, and failure classification. The re-frame.http namespace holds the verb-helper synthesis fns (get, post, put, delete, patch, head, options) that build that fx. The fx itself, its abort/canned siblings, the request-interceptor middleware, and the test stubs are keyword-addressed or re-exported on the re-frame.core façade.
The CLJS reference ships managed HTTP (Fetch in the browser, java.net.http.HttpClient on the JVM). Ports that omit it must not reuse the :rf.http/* namespace for anything else.
See Managed HTTP — The request is a map for the teaching guide.
The verb helpers live in re-frame.http. The :rf.http/managed fx is keyword-addressed — you use it in an event's :fx. The interceptor and test-stub fns (reg-http-interceptor, clear-http-interceptor, with-managed-request-stubs) are re-exported on the re-frame.core façade, so the examples below also require [re-frame.core :as rf]. Everything ships in the day8/re-frame2-http artefact.
Keyword surfaces — the managed-HTTP fx¶
[:rf.http/managed args-map]¶
- Kind: fx
- Args: per Managed HTTP — The request is a map and
:rf.fx/managed-args - Description: The one managed-HTTP fx-id. Keys in the args map:
:request— the request envelope;:urlis required. A missing / nil / blank final URL raises:rf.error/http-bad-requestat dispatch, after the:beforechain runs.:decode— decode policy.:accept— accept fn.:retry—{:on #{categories} :max-attempts N :backoff {:base-ms :factor :max-ms :jitter}}.:onmust be a set drawn from the retryable subset#{:rf.http/transport :rf.http/cors :rf.http/timeout :rf.http/http-4xx :rf.http/http-5xx}. Anything else raises:rf.error/http-bad-retry-onat dispatch.:timeout-ms— per-attempt timeout, default 30000. An explicitnilor0opts out.:reply-to— the unified reply target: one event vector for both the success and the failure reply (the app branches on:status). The same call-site key resources / mutations use.:on-success/:on-failure— the split routing sugar over:reply-to: success / failure target events; event vector or nil. Any other value raises:rf.error/http-bad-reply-target. Supplying none of:reply-to/:on-success/:on-failureraises:rf.error/http-no-reply-targetat dispatch.:request-id— for abort / supersede.:abort-signal— optional (CLJS).:sensitive?— see Privacy and classification.
[:rf.http/managed-abort request-id]¶
- Kind: fx
- Args: request-id
- Description: Abort the in-flight request with the given
:request-id. The aborted request's reply is the canonical{:status :cancelled :cancel/reason :user :error {:kind :rf.http/aborted ...}}envelope, appended to the request's reply target (:reply-toor:on-failure) per Reply addressing. - Example:
A minimal request¶
(rf/reg-event :cart/load
(fn [_ _]
{:fx [[:rf.http/managed
{:request {:method :get :url "/api/cart"}
:on-success [:cart/loaded]
:on-failure [:cart/load-failed]}]]}))
(rf/reg-event :cart/loaded
(fn [{:keys [db]} [_ {:keys [value]}]]
{:db (assoc-in db [:cart :items] value)}))
(rf/reg-sub :cart/items
(fn [db _] (get-in db [:cart :items])))
Retries, timeouts, schema validation, abort, decode customisation, and accept-fn refinement are optional keys in the args map.
Reply addressing¶
Every reply is the one canonical reply envelope — a plain map keyed on a closed :status:
;; Success
{:status :ok :value decoded-body …}
;; Failure — per-kind tags (:status / :status-text / :message / ...) ride
;; flat on the failure map alongside :kind
{:status :error
:error {:kind :rf.http/<category> ...}}
;; Abort / cancel
{:status :cancelled :cancel/reason :user
:error {:kind :rf.http/aborted …}}
Every request must address its reply; where the payload lands depends on how:
- Unified
:reply-to— one event vector for both the success and the failure reply, the canonical envelope appended as the last arg. A:cart/loadhandler sees[:cart/load {:status :ok :value decoded-body …}](or the:error/:cancelledshape) and branches on(:status reply). Ride request context along the prefix —:reply-to [:cart/load ctx]delivers[:cart/load ctx <envelope>]. The same call-site key resources / mutations use. - Split
:on-success/:on-failure— pure routing sugar over:reply-to. Both receive the identical envelope appended as the last event-vector arg. A:cart/loadedhandler sees[:cart/loaded {:status :ok :value decoded-body …}]and destructures it directly ([_ {:keys [value]}]for success,[_ {:keys [error]}]for failure). :on-success nil/:on-failure nil— silences that side; no reply dispatch.- Omitting all of
:reply-to/:on-success/:on-failureraises:rf.error/http-no-reply-targetat dispatch — the co-located default (reply merged under:rf/replyback to the originating event) was retired pre-alpha; the framework never silently addresses the reply. - Any other non-vector value for a reply-target key raises
:rf.error/http-bad-reply-target.
Both shapes are detailed in Managed HTTP — Handling the reply.
Failure categories (closed set)¶
Eight failure :kind values, all reserved under :rf.http/*. The set is closed, so a handler's failure switch can be exhaustive.
:kind |
Meaning |
|---|---|
:rf.http/transport |
Network / DNS / connection error pre-HTTP. |
:rf.http/cors |
CORS preflight rejected (CLJS-only). |
:rf.http/timeout |
Per-attempt timeout fired. |
:rf.http/http-4xx |
Non-2xx 4xx response. |
:rf.http/http-5xx |
Non-2xx 5xx response. |
:rf.http/decode-failure |
2xx response but decode rejected the body. |
:rf.http/accept-failure |
:accept returned {:failure user-map}. |
:rf.http/aborted |
Request aborted via :request-id or :abort-signal. |
See Managed HTTP — Failures are a closed set for tags-by-kind.
Verb helpers¶
Call-site helpers that synthesise the canonical [:rf.http/managed args-map] fx vector. Pure; no side effect — drop the result into :fx.
Every helper takes url and a required args map. args MUST address the reply — supply :reply-to, :on-success, or :on-failure (an unaddressed request fails loud at dispatch with :rf.error/http-no-reply-target). {:reply-to nil} is the explicit fire-and-forget spelling. There is no URL-only arity: it would build an effect guaranteed to fail its own boundary validation.
get¶
- Kind: function
- Signature:
- Description: Synthesise a GET fx vector.
argsmerges top-level into the canonical args map, and the caller wins. The exception is:request: it merges with{:method :get :url url}, and the helper's:methodand:urloverwrite caller-supplied ones. All seven helpers share this merge contract.
post¶
- Kind: function
- Signature:
- Description: POST. Pass
:bodyinargs. - Example:
put¶
- Kind: function
- Signature:
- Description: PUT.
- Example:
delete¶
- Kind: function
- Signature:
- Description: DELETE.
- Example:
patch¶
- Kind: function
- Signature:
- Description: PATCH.
- Example:
head¶
- Kind: function
- Signature:
- Description: HEAD.
- Example:
options¶
- Kind: function
- Signature:
- Description: OPTIONS.
- Example:
The verb helpers require [re-frame.http :as rf.http] alongside re-frame.core. The namespace ships in the day8/re-frame2-http artefact — the same artefact as the fx.
{:fx [(rf.http/get "/api/cart"
{:on-success [:cart/loaded]
:on-failure [:cart/load-failed]
:retry {:on #{:rf.http/transport :rf.http/timeout}
:max-attempts 3
:backoff {:base-ms 100}}})]}
Request-interceptor middleware¶
A middleware surface that mirrors the rest of the reg-* family. Use it to inject behaviour into every request: an auth header, a request-id stamp, logging. The fn forms (reg-http-interceptor, clear-http-interceptor) are re-exported on the re-frame.core façade. A façade call with day8/re-frame2-http absent raises :rf.error/http-artefact-missing. The data-shaped :rf.fx/* siblings below are keyword-addressed.
reg-http-interceptor¶
- Kind: macro
- Signature:
- Description: Register an HTTP interceptor on a frame's
:rf.http/managedmiddleware chain. Returnsid. interceptor-mapcarries at least one of:before (fn [ctx] ctx')(request-side) and:after (fn [ctx response] response')(response-side), plus optional:frame(explicit-frame override) and the standard:rf/registration-metadata.- The target frame is the explicit
:framewhen given, else the carried scope it registers under (with-frame/ an:initial-eventsstep). Registering under no scope raises:rf.error/no-frame-context— there is no:rf/defaultdefault. - An invalid shape (non-keyword id, neither
:beforenor:after, a non-fn slot, a non-keyword:frame) raises:rf.error/http-bad-interceptor. - Re-registering an existing id replaces the slot in place, position preserved. After a
clear-http-interceptor, re-registering the same id appends to the end of the chain. - The
:beforechain runs in registration order; the:afterchain runs in reverse registration order.:aftersees the same ctx the:beforechain produced (request-correlated handling).
clear-http-interceptor¶
- Kind: function
- Signature:
- Description: Unregister an interceptor by id.
(clear-http-interceptor id)resolves the frame from the carried scope it runs under.(clear-http-interceptor id {:frame target})names the frame explicitly via the trailing opts map, mirroringreg-http-interceptor's:frameand the family's public-frame-targeting law (never a positional frame arg on a public surface).- Either form raises
:rf.error/no-frame-contextwhen the frame is absent (single-arity under no scope, or opts{:frame nil}). It never clears against a synthesised:rf/default. - The 2-arity is shape-discriminated: an opts map as the second arg is the public form, while a bare frame target in the first position is the internal frame-first
(frame id)plumbing. - Example:
[:rf.fx/reg-http-interceptor args-map]¶
- Kind: fx
- Args:
{:id <kw> :before <fn>? :after <fn>? :frame <id>? ...}—:idplus the same interceptor-map slots as the fn form, including:rf/registration-metadata - Description: Data-shaped sibling of
reg-http-interceptor, for callers driving registration through the dispatch surface (EDN conformance fixtures, boot event handlers). The fx body splits:idoff and passes the remaining map to the fn form. When the args omit:frame, the fx-context frame (the cascade envelope stamp) is threaded in. Registered at load ofre-frame.http.managed; dev+prod. - Example:
[:rf.fx/clear-http-interceptor args-map]¶
- Kind: fx
- Args:
{:id <kw> :frame <id>?} - Description: Data-shaped sibling of
clear-http-interceptor. The args:framewins, else the fx-context frame. Registered at load ofre-frame.http.managed; dev+prod. - Example:
(rf/reg-http-interceptor :auth/inject
{:before (fn [{:keys [request] :as ctx}]
(assoc-in ctx [:request :headers "Authorization"]
(str "Bearer " (token-from-app-db))))})
;; Or with both sides — :before stamps a start mark, :after reads it.
(rf/reg-http-interceptor :telemetry
{:before (fn [ctx] (assoc ctx ::started (js/Date.now)))
:after (fn [ctx resp]
(assoc resp :elapsed-ms (- (js/Date.now) (::started ctx))))})
:before runs before the request is dispatched to the platform's HTTP client. :after runs after the response is built and before :on-success / :on-failure fire. If either throws, the corresponding side is not delivered: a throwing :before means the request is not dispatched; a throwing :after means the reply is suppressed. In both cases :rf.error/http-interceptor-failed fires with :frame, :interceptor-id, :url, and :cause. On the response side it also stamps :phase :after; that tag is absent for :before. See Managed HTTP — Interceptors.
Testing: stubbed responses¶
Test-support surface for driving the pipeline without the network: canned-reply fx, plus a stubbing macro that reroutes requests at named routes. The with-managed-request-stubs macro is re-exported on the re-frame.core façade. The raw install / uninstall pair is reached only through the home namespace re-frame.http.test-support.
[:rf.http/managed-canned-success {:value v}]¶
- Kind: fx
- Description: Synthesise the canonical success reply (
{:status :ok :value v}) directly into:fx, for inline "stub THIS request" patterns. :valuedefaults to{:stubbed true}when absent.:after-ms(optional) — a positive value defers the reply via a:dispatch-latertick. Absent,0, or non-positive delivers immediately.- Runs the frame's
:beforeand:afterinterceptor chains around the synthesised reply, exactly like the real transport path. Reply addressing (:reply-to/:on-success) applies as for:rf.http/managed; an unaddressed stub reply is silently dropped. - Registered at load of
re-frame.http.test-support. - Example:
[:rf.http/managed-canned-failure {:kind <:rf.http/*> :tags {...}}]¶
- Kind: fx
- Description: Synthesise the canonical failure reply directly into
:fx. :kinddefaults to:rf.http/transport.:tagsmerge into the:errormap.- An
:rf.http/abortedkind yields:status :cancelled; every other kind yields:status :error. - Supports the same optional
:after-msdeferral, interceptor-chain behaviour, and reply addressing (:reply-to/:on-failure) as:rf.http/managed-canned-success. - Registered at load of
re-frame.http.test-support. - Example:
with-managed-request-stubs¶
- Kind: macro
- Signature:
- Description: Lexical-scope stubbing.
route-mapis{[<method> <url>] {:reply {:ok <value>}}}(success) or{[<method> <url>] {:reply {:failure <failure-map>}}}(failure).- Inside the body, requests matching a stubbed route bypass the real client. The helper registers a per-scope stub fx with a process-unique
:rf.test/managed-http-stub-<n>id, under the test-runner-internal:rf.test/*fx-stub family, so nested scopes compose. It then installs the matching:rf.http/managedoverride for the body's dynamic extent. - Plain
dispatch-synccalls auto-route by method + URL with no manual:fx-overrides. A per-call:fx-overridesstill wins. - Routes match against the post-
:beforerequest (the method + URL the pipeline would actually issue). A request with no matching route receives a synthesised:rf.http/transportfailure reply. - The façade macro needs
re-frame.http.test-supportin the require closure. Without it, the call raises:rf.error/http-artefact-missing.
with-managed-request-stubs*¶
- Kind: function
- Signature:
- Description: Plain-fn surface beneath the macro, for computed route-maps or non-literal bodies. Like the macro, it installs the
:rf.http/managedoverride forbody-fn's dynamic extent, so dispatches inside auto-route with no manual:fx-overrides. - Example:
re-frame.http.test-support/install-managed-request-stubs!¶
- Kind: function
- Signature:
- Description: Lower-level than
with-managed-request-stubs. Use it when stubs span multipledeftests. It registers the:rf.http/managed-test-stubfx — the stable, documented:fx-overridestarget — which persists untiluninstall-managed-request-stubs!. Returns the stub fx-id. - Unlike the wrapper, this does not install the
:rf.http/managedoverride. Dispatch with{:fx-overrides {:rf.http/managed :rf.http/managed-test-stub}}(or wrap dispatches inwith-fx-overrides) to route through it. - Nested installs snapshot the prior handler and restore it on uninstall (LIFO).
- Not a
re-frame.corefaçade export — call it through its home namespacere-frame.http.test-support. - Example:
re-frame.http.test-support/uninstall-managed-request-stubs!¶
- Kind: function
- Signature:
- Description: Tear down the most recent install. When installs were nested, it restores the previously installed stub handler (LIFO); otherwise it clears the stub fx and restores real-request routing. Idempotent — a teardown without a matching install is a safe no-op. Not a
re-frame.corefaçade export — call it through its home namespacere-frame.http.test-support. - Example:
All test-support surfaces live in re-frame.http.test-support — one namespace, same artefact (day8/re-frame2-http) as the production code.
(deftest cart-loads
(with-managed-request-stubs
{[:get "/api/cart"] {:reply {:ok [{:id 1 :name "widget"}]}}}
(rf/dispatch-sync [:cart/load])
(is (= 1 (count (subscribe-once [:cart/items]))))))
Schema-reflection metadata¶
Handlers may declare :rf.http/decode-schemas [<schema> ...] in their reg-event metadata-map; pair tools and generators read it via (rf/handler-meta :event id). Optional, never enforced — pure metadata for tooling. See re-frame.schemas.md.
Privacy and classification¶
HTTP carries secrets: passwords in request bodies, auth tokens in request headers, PII in response bodies. The framework keeps these off its own observability wire — traces, off-box records, SSR payloads. Four declaration surfaces cooperate, none of them a process-global mutation. See Managed HTTP — Keeping secrets out of the trace and keep secrets out of traces for the model end-to-end.
| Surface | What it covers | Where declared |
|---|---|---|
| Built-in header denylist | A closed, immutable set of always-sensitive header names (Authorization, Cookie, Set-Cookie, X-API-Key, X-Auth-Token, X-CSRF-Token, …). Redacted in every :rf.http/* trace's :headers slot, regardless of any :sensitive? flag. Matching is case-insensitive. No frame can remove a name. |
framework default |
| Built-in query-param denylist | A closed, immutable set of always-sensitive query-param names (api_key, access_token, token, secret, password, session, signature, …). The value is redacted inline in :url slots (?api_key=:rf/redacted&page=2), with name and position preserved. A hit also stamps :sensitive? true on the event; the name is the signal. |
framework default |
| Managed-HTTP carriers | App-specific sensitive header / query-param names, declared on the :rf.http/managed reg-fx registration (the transient-payload case). :headers is a vector of names — vector-only, because header names always union onto the built-ins. :query-params is a vector of names (union) OR an {:include […] :except […]} policy map with effective policy (defaults − except) ∪ include; :except subtracts a built-in query-param default for this app's own dev trace. Names are case-insensitive. A malformed block fails loud. |
reg-fx :rf.http/managed :carriers {:headers […] :query-params […]} |
Per-request / per-call :sensitive? |
The coarse opt-in that redacts a single request's body / params / all URL param values wholesale. | the :rf.http/managed args map (:sensitive? at top level, or under :request) |
;; managed-HTTP carrier extensions (union onto the immutable built-ins)
(rf/reg-fx :rf.http/managed
{:carriers {:headers ["X-Honeycomb-Team"]
:query-params ["shop_token"]}}
re-frame.http.managed/managed-handler)
;; per-call opt-in for a single sensitive request
(rf/reg-event :api/login
(fn [_ [_ creds]]
{:fx [[:rf.http/managed
{:request {:method :post :url "/auth/login" :body creds}
:sensitive? true}]]}))
re-frame.http.managed/managed-handler¶
- Kind: function
- Signature:
- Description: The
:rf.http/managedfx handler body. It is re-exported so an app can re-register:rf.http/managedto declare its:carriersblock (as in the example above) without knowing the internal handler fn. Do not call it directly from app code — pass it toreg-fx.
Response-body classification — on the :decode schema¶
The denylists and :sensitive? flag cover request carriers. The response body is a registration-owned transient payload, classified per-slot via :sensitive? / :large? props on the request's :decode schema. The :decode schema is the owner's natural declaration of the body shape, so per-slot props are the one route to classify it. These props fire independently of the per-call :sensitive? flag.
(rf/reg-event :auth/login
(fn [_ [_ creds]]
{:fx [[:rf.http/managed
{:request {:method :post :url "/auth/login" :body creds}
;; [:token] redacts; [:user-id] rides verbatim
:decode [:map
[:token {:sensitive? true} :string]
[:user-id :int]]
:on-success [:auth/logged-in]}]]}))
- Per-slot. A
:sensitive?slot redacts to:rf/redacted. A:large?slot elides to the size marker. When a slot carries both, sensitive wins. A non-marked sibling rides verbatim. A root-level prop classifies the whole body — e.g.[:string {:sensitive? true}]for an opaque-token response. - Off-box fail-closed. Only an introspectable Malli schema (the raw EDN
[op props? …]vector form) carries per-slot marks the walker can read. An unschematized body has an unknown shape, so it is omitted entirely off-box (fail-closed) rather than shipped raw. Unschematized means any of: the keyword decode modes (:json/:text/ …), a custom decoder fn, a registry-keyword ref, or a compiledm/schemaobject. - Raw error bodies are unconditionally omitted off-box. A 4xx/5xx
:bodyand a decode-failure:body-textare never decoded, because status classification runs before decode. They therefore fail closed off-box irrespective of:sensitive?. Error bodies frequently echo request context or tokens.
Classification does not propagate — declare each surface a secret crosses
A token in the response body (classified by the :decode schema) and the same token stored durably in app-db are two declarations on two surfaces. There is no propagation that carries one to the other. When :on-success writes the token into app-db, classify that durable path too: a reg-event returning :sensitive [[:auth :token]] alongside :db (re-frame.core.md §Standard events and keep secrets out of traces). And never copy a secret into a sibling app-db path you have not classified — e.g. a JWT duplicated at [:auth :user :token]. The copy ships raw until that path is classified or the duplicate is dropped.
Trace events emitted by :rf.http/managed¶
:operation |
:op-type |
When |
|---|---|---|
:rf.http/retry-attempt |
:info |
Per intermediate attempt that matched :retry :on. Carries :request-id, :url, :attempt, :max-attempts, :failure, :next-backoff-ms (nil on the final exhaustion row). |
:rf.http.interceptor/registered |
:info |
A reg-http-interceptor succeeded. Carries :frame, :id. |
:rf.http.interceptor/cleared |
:info |
A clear-http-interceptor removed an existing slot. Carries :frame, :id. |
:rf.error/http-interceptor-failed |
:error |
An interceptor :before or :after threw. Carries :frame, :interceptor-id, :url, :cause (plus :phase :after on the response side). Request side: the request is NOT dispatched; response side: the reply is suppressed. |
See also¶
- re-frame.core.md —
:rf.http/managedrowed in the standard fx table;:sensitiveon Standard events; and the instrumentation/egress surface (project-egress, the wire-boundary walker, the observability-sink registration). - re-frame.schemas.md —
:rf.http/decode-schemas, the:schemametadata key, and per-slot:sensitive?/:large?schema props. - re-frame.test-support.md — patterns for combining HTTP stubs with
dispatch-syncandpoll-until. - Keep secrets out of traces — the full classification + projection model.
- Managed HTTP reference — the guide-level reference; Interceptors and secrets carries §Keeping secrets out of the trace.