Skip to content

07 — HTTP

Managed HTTP is the answer to "I want my app to talk to a server, but I don't want to write a custom fx-handler every time, and I want retries / cancellation / timeouts / decode / failure-classification to be the framework's problem, not mine." :rf.http/managed is one fx-id that takes one args map and gives you back one reply event with one closed taxonomy of failure kinds.

You don't get this for free — Spec 014 is an optional capability. Implementations ship it (the CLJS reference does, on Fetch in the browser and java.net.http.HttpClient on the JVM); ports that omit it must not reuse the :rf.http/* namespace for anything else. If you're using the CLJS reference, you have it; if you're vendoring a port that doesn't, you're back to writing your own.

This chapter covers the canonical fx, the verb helpers, the test stubs, the request-interceptor surface, and the closed failure taxonomy. The normative source — args map, decode pipeline, retry semantics, reply addressing — lives at 014-HTTPRequests.md.

The canonical fx

[:rf.http/managed args-map]

  • Kind: fx
  • Args: per 014 §The args map and :rf.fx/managed-args
  • Description: The one fx-id. Args carry the request envelope, decode policy, accept fn, retry policy, timeout, success / failure target events, request-id (for abort), and optional abort-signal.
  • In the wild: managed_http_counter · realworld

[: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 fires with {:rf/reply {:kind :failure :failure {:kind :rf.http/aborted ...}}}.

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 [rf/reply]}]]
    {:db (assoc-in db [:cart :items] (:value reply))}))

That's enough to issue a request, decode the JSON reply, and dispatch the result back. Retries, timeouts, schema validation, abort, decode customisation, accept-fn refinement — all of those are optional keys in the args map; you reach for them when the problem asks for them.

Verb helpers

Call-site helpers for the common shapes. They're pure synthesis fns that produce the canonical [:rf.http/managed args-map] fx vector — no magic, no hidden state. The point is ergonomics: writing (rf.http/get "/api/cart") reads better at the call site than spelling out the full args map for a no-frills GET.

re-frame.http/get

  • Signature:
    (rf.http/get url)
    (rf.http/get url args)
    
  • Description: "Synthesise a GET fx vector." Pure; no side effect — drop the result into :fx.

re-frame.http/post

  • Signature:
    (rf.http/post url)
    (rf.http/post url args)
    
  • Description: POST. Pass :body in args.

re-frame.http/put

  • Signature:
    (rf.http/put url)
    (rf.http/put url args)
    
  • Description: PUT.

re-frame.http/delete

  • Signature:
    (rf.http/delete url)
    (rf.http/delete url args)
    
  • Description: DELETE.

re-frame.http/patch

  • Signature:
    (rf.http/patch url)
    (rf.http/patch url args)
    
  • Description: PATCH.

re-frame.http/head

  • Signature:
    (rf.http/head url)
    (rf.http/head url args)
    
  • Description: HEAD.

re-frame.http/options

  • Signature:
    (rf.http/options url)
    (rf.http/options url args)
    
  • Description: OPTIONS.

The verb helpers live in re-frame.http — users (:require [re-frame.http :as rf.http]) alongside re-frame.core. The namespace ships in day8/re-frame2-http, the same artefact as the fx itself, so loading the helpers and the fx is a single dep decision.

{: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-ms 100}})]}

Reply addressing

Every reply lands under :rf/reply in the dispatched event's payload map. Two shapes:

;; Success
{:rf/reply {:kind :success :value decoded-body}}

;; Failure
{:rf/reply {:kind    :failure
            :failure {:kind  :rf.http/<category>
                      :tags  {...}}}}

Default reply addressing dispatches [<originating-event-id> (assoc original-msg :rf/reply ...)] back to the same handler — your :cart/load handler sees the reply at :rf/reply. Explicit :on-success / :on-failure targets append the reply payload as the last event-vector arg — your :cart/loaded handler sees [:cart/loaded {:rf/reply ...}]. Both shapes detailed in 014 §Reply addressing.

Failure categories (closed set)

Eight failure :kind values, all reserved under :rf.http/*. The set is closed — ports that ship Spec 014 deliver exactly these eight categories, and your 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 014 §Failure categories for tags-by-kind.

Request-interceptor middleware

Sometimes you want to inject behaviour into every request — adding an auth header, stamping a request ID, logging. Re-frame2's answer is a small middleware surface that mirrors the rest of the reg-* family.

reg-http-interceptor

  • Kind: function
  • Signature:
    (reg-http-interceptor id interceptor-map)
    
  • Description: Register an HTTP interceptor on a frame's :rf.http/managed middleware chain. interceptor-map carries at least one of :before (fn [ctx] ctx') (request-side) and :after (fn [ctx response] response') (response-side), plus optional :frame (the EP-0002 override) and the standard :rf/registration-metadata. The target frame is the explicit :frame, else the carried scope it registers under (with-frame / an :on-create hook); registering under no scope raises :rf.error/no-frame-context — there is no :rf/default default. 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).
  • In the wild: realworld

clear-http-interceptor

  • Kind: function
  • Signature:
    (clear-http-interceptor id)
    (clear-http-interceptor frame id)
    
  • Description: Unregister an interceptor by id. The single-arity (clear-http-interceptor id) resolves the frame from the carried scope it runs under; the two-arity (clear-http-interceptor frame id) names the frame explicitly. Either form raises :rf.error/no-frame-context when the frame is absent (single-arity under no scope, or two-arity passed nil) — it never clears against a synthesised :rf/default.
(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))))})

The :before runs before the request is dispatched to the platform's HTTP client; the :after runs after the response is built and BEFORE :on-success / :on-failure fire. If either throws, the corresponding side is not delivered (request: not dispatched; response: reply suppressed) and :rf.error/http-interceptor-failed fires with :frame, :interceptor-id, :url, :cause, and :phase. See 014 §Middleware.

Testing: stubbed responses

Tests want to drive the cascade without hitting the network. The test-support surface provides canned-reply fx and a stubbing macro that reroutes requests at the routes you name.

[:rf.http/managed-canned-success {:value v}]

  • Kind: fx
  • Description: Synthesise the canonical success reply directly into :fx. Useful for "stub THIS request inline" patterns. Registered at load of re-frame.http-test-support.

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

  • Kind: fx
  • Description: Synthesise the canonical failure reply directly into :fx.

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 installs the :rf.http/managed → :rf.http/managed-test-stub override for the body, so plain dispatch-sync calls auto-route by method + URL with NO manual :fx-overrides.

with-managed-request-stubs*

  • Kind: function
  • Signature:
    (with-managed-request-stubs* route-map body-fn)
    
  • Description: Plain-fn surface beneath the macro. Use 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.

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: registers the :rf.http/managed-test-stub fx that persists until uninstall-managed-request-stubs!. Use when stubs span multiple deftests. 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. Not a re-frame.core façade export — call it through its home namespace re-frame.http-test-support.

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

  • Kind: function
  • Signature:
    (uninstall-managed-request-stubs!)
    
  • Description: Drop installed stubs; restore real-request routing. Idempotent. Not a re-frame.core façade export — call it through its home namespace re-frame.http-test-support.

All the test-support surfaces live in re-frame.http-test-support (the single home per audit of audits #15). One namespace; same artefact (day8/re-frame2-http) as the production code. The ergonomic 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 (rf2-ntwwyt).

(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 014 §Schema reflection.

Trace events emitted by :rf.http/managed

:operation :op-type When
:rf.http/retry-attempt :info Per intermediate attempt that matched :retry :on. Carries :attempt, :max-attempts, :failure, :next-backoff-ms.
: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.
:rf.error/http-interceptor-failed :error A request-interceptor :before threw. Carries :frame, :interceptor-id, :url, :cause. The request is NOT dispatched.

See also