Skip to content

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.

(:require [re-frame.http :as rf.http])

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; :url is required. A missing / nil / blank final URL raises :rf.error/http-bad-request at dispatch, after the :before chain runs.
  • :decode — decode policy.
  • :accept — accept fn.
  • :retry{:on #{categories} :max-attempts N :backoff {:base-ms :factor :max-ms :jitter}}. :on must 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-on at dispatch.
  • :timeout-ms — per-attempt timeout, default 30000. An explicit nil or 0 opts 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-failure raises :rf.error/http-no-reply-target at 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-to or :on-failure) per Reply addressing.
  • Example:
    (rf/reg-event :request/abort
      (fn [_ [_ request-id]]
        {:fx [[:rf.http/managed-abort request-id]]}))
    

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/load handler sees [:cart/load {:status :ok :value decoded-body …}] (or the :error / :cancelled shape) 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/loaded handler 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-failure raises :rf.error/http-no-reply-target at dispatch — the co-located default (reply merged under :rf/reply back 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:
    (rf.http/get url args)
    
  • Description: Synthesise a GET fx vector. args merges 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 :method and :url overwrite caller-supplied ones. All seven helpers share this merge contract.

post

  • Kind: function
  • Signature:
    (rf.http/post url args)
    
  • Description: POST. Pass :body in args.
  • Example:
    {:fx [(rf.http/post "/api/items"
                        {:request    {:body {:title "new"}
                                      :request-content-type :json}
                         :on-success [:items/created]})]}
    

put

  • Kind: function
  • Signature:
    (rf.http/put url args)
    
  • Description: PUT.
  • Example:
    {:fx [(rf.http/put "/api/items/42"
                       {:request    {:body {:title "updated"}
                                     :request-content-type :json}
                        :request-id [:item :update 42]})]}
    

delete

  • Kind: function
  • Signature:
    (rf.http/delete url args)
    
  • Description: DELETE.
  • Example:
    {:fx [(rf.http/delete "/api/items/42"
                          {:on-failure [:item/delete-failed]})]}
    

patch

  • Kind: function
  • Signature:
    (rf.http/patch url args)
    
  • Description: PATCH.
  • Example:
    {:fx [(rf.http/patch "/api/items/42"
                         {:request    {:body {:done true}
                                       :request-content-type :json}
                          :on-success [:items/patched]})]}
    
  • Kind: function
  • Signature:
    (rf.http/head url args)
    
  • Description: HEAD.
  • Example:
    {:fx [(rf.http/head "/api/items"
                        {:on-success [:items/exists?]})]}
    

options

  • Kind: function
  • Signature:
    (rf.http/options url args)
    
  • Description: OPTIONS.
  • Example:
    {:fx [(rf.http/options "/api/items" {:on-success [:api/capabilities]})]}
    

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:
    (reg-http-interceptor id interceptor-map)
    
  • Description: Register an HTTP interceptor on a frame's :rf.http/managed middleware chain. Returns id.
  • interceptor-map carries 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 :frame when given, else the carried scope it registers under (with-frame / an :initial-events step). Registering under no scope raises :rf.error/no-frame-context — there is no :rf/default default.
  • An invalid shape (non-keyword id, neither :before nor :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 :before chain runs in registration order; the :after chain runs in reverse registration order. :after sees the same ctx the :before chain produced (request-correlated handling).

clear-http-interceptor

  • Kind: function
  • Signature:
    (clear-http-interceptor id)
    (clear-http-interceptor id {:frame target})
    
  • 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, mirroring reg-http-interceptor's :frame and the family's public-frame-targeting law (never a positional frame arg on a public surface).
  • Either form raises :rf.error/no-frame-context when 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/clear-http-interceptor :auth-header)
    

[:rf.fx/reg-http-interceptor args-map]

  • Kind: fx
  • Args: {:id <kw> :before <fn>? :after <fn>? :frame <id>? ...}:id plus 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 :id off 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 of re-frame.http.managed; dev+prod.
  • Example:
    {:fx [[:rf.fx/reg-http-interceptor
           {:id     :auth/inject
            :before (fn [ctx] (assoc-in ctx [:request :headers "Authorization"] token))}]]}
    

[:rf.fx/clear-http-interceptor args-map]

  • Kind: fx
  • Args: {:id <kw> :frame <id>?}
  • Description: Data-shaped sibling of clear-http-interceptor. The args :frame wins, else the fx-context frame. Registered at load of re-frame.http.managed; dev+prod.
  • Example:
    {:fx [[:rf.fx/clear-http-interceptor {:id :auth/inject}]]}
    
(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.
  • :value defaults to {:stubbed true} when absent.
  • :after-ms (optional) — a positive value defers the reply via a :dispatch-later tick. Absent, 0, or non-positive delivers immediately.
  • Runs the frame's :before and :after interceptor 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/reg-event :counter/retry-recover
      (fn [_ _]
        {:fx [[:rf.http/managed-canned-success
               {:request  {:method :get :url "api/flaky"}
                :decode   :json
                :value    {:delta 5}
                :reply-to [:counter/retry-recover]}]]}))
    

[:rf.http/managed-canned-failure {:kind <:rf.http/*> :tags {...}}]

  • Kind: fx
  • Description: Synthesise the canonical failure reply directly into :fx.
  • :kind defaults to :rf.http/transport. :tags merge into the :error map.
  • An :rf.http/aborted kind yields :status :cancelled; every other kind yields :status :error.
  • Supports the same optional :after-ms deferral, 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:
    (rf/reg-event :flaky/load
      (fn [_ _]
        {:fx [[:rf.http/managed-canned-failure
               {:on-failure [:flaky/load-error]
                :kind       :rf.http/http-5xx
                :tags       {:status 503 :status-text "Service Unavailable"}}]]}))
    

with-managed-request-stubs

  • Kind: macro
  • Signature:
    (with-managed-request-stubs route-map body+)
    
  • Description: Lexical-scope stubbing.
  • route-map is {[<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/managed override for the body's dynamic extent.
  • Plain dispatch-sync calls auto-route by method + URL with no manual :fx-overrides. A per-call :fx-overrides still wins.
  • Routes match against the post-:before request (the method + URL the pipeline would actually issue). A request with no matching route receives a synthesised :rf.http/transport failure reply.
  • The façade macro needs re-frame.http.test-support in the require closure. Without it, the call raises :rf.error/http-artefact-missing.

with-managed-request-stubs*

  • Kind: function
  • Signature:
    (with-managed-request-stubs* route-map body-fn)
    
  • Description: Plain-fn surface beneath the macro, for computed route-maps or non-literal bodies. Like the macro, it installs the :rf.http/managed override for body-fn's dynamic extent, so dispatches inside auto-route with no manual :fx-overrides.
  • Example:
    ;; Computed route-map / non-literal body — use the fn form.
    (rf/with-managed-request-stubs*
      {[:get "/articles"] {:reply {:ok [:hello :world]}}}
      (fn []
        ;; No manual :fx-overrides — auto-routes by method + URL.
        (rf/dispatch-sync [:articles/list])))
    

re-frame.http.test-support/install-managed-request-stubs!

  • Kind: function
  • Signature:
    (install-managed-request-stubs! route-map)
    
  • Description: Lower-level than with-managed-request-stubs. Use it when stubs span multiple deftests. It registers the :rf.http/managed-test-stub fx — the stable, documented :fx-overrides target — which persists until uninstall-managed-request-stubs!. Returns the stub fx-id.
  • Unlike the wrapper, this does not install the :rf.http/managed override. Dispatch with {:fx-overrides {:rf.http/managed :rf.http/managed-test-stub}} (or wrap dispatches in with-fx-overrides) to route through it.
  • Nested installs snapshot the prior handler and restore it on uninstall (LIFO).
  • Not a re-frame.core façade export — call it through its home namespace re-frame.http.test-support.
  • Example:
    ;; Stubs that span several deftests — install once, route via :fx-overrides.
    (re-frame.http.test-support/install-managed-request-stubs!
      {[:get "/api/cart"] {:reply {:ok [{:id 1 :name "widget"}]}}})
    
    (rf/dispatch-sync [:cart/load]
                      {:fx-overrides {:rf.http/managed :rf.http/managed-test-stub}})
    

re-frame.http.test-support/uninstall-managed-request-stubs!

  • Kind: function
  • Signature:
    (uninstall-managed-request-stubs!)
    
  • 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.core façade export — call it through its home namespace re-frame.http.test-support.
  • Example:
    (re-frame.http.test-support/uninstall-managed-request-stubs!)
    

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:
    (managed-handler frame-ctx args-map)
    
  • Description: The :rf.http/managed fx handler body. It is re-exported so an app can re-register :rf.http/managed to declare its :carriers block (as in the example above) without knowing the internal handler fn. Do not call it directly from app code — pass it to reg-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 compiled m/schema object.
  • Raw error bodies are unconditionally omitted off-box. A 4xx/5xx :body and a decode-failure :body-text are 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