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. The public surface is exact — the two shapes above, nothing else.
(clear-http-interceptor id)resolves the frame through the ambient (carried-scope) chain it runs under. Under no scope it raises the always-on:rf.error/no-frame-contextrather than clearing against a synthesised:rf/default.(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).targetis a present, non-nil frame-id keyword or a live frame value.- The two-arity opts map is fail-closed: it MUST be exactly
{:frame target}. A{}, a{:frame nil}, a typo'd or extra key ({:fram f}), and a non-map second arg all raise:rf.error/http-bad-interceptorBEFORE any ambient frame is resolved or touched — never reinterpreted as a positional frame nor silently cleared against the ambient scope (rf2-s32bf). This is distinct from the single-arity no-scope:rf.error/no-frame-context. - The frame-first
(frame id)spelling is a separate artefact-internal seam (clear-http-interceptor*), reached directly by internal cleanup that already holds a resolved frame — e.g. the:rf.fx/clear-http-interceptorfx — not a public arity ofclear-http-interceptor. - 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 binds one stable override target —
:rf.test/managed-http-scope-stub, in the test-runner-internal:rf.test/*fx-stub family, registered once whenre-frame.http.test-supportloads — as the:rf.http/managedoverride for the body's dynamic extent, and carries the scope's route map on a dynamic var. It mints no per-scope fx and performs no registrar write; nested scopes compose because an inner scope's route-map binding shadows the outer's and restores it on exit. Because the target is registered at load time rather than minted per scope, it resolves through the sealed image generation of a frame created before the scope was entered — adispatch-syncin a pre-created frame still routes through the stub. - 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 binds the
:rf.http/managedoverride (to the stable:rf.test/managed-http-scope-stubtarget) plus the scope's route map 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 bind 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.