Summary
There's no first-class way to stub httx responses in tests. Today you either hit the real network (flaky, slow, needs credentials) or hand-roll a globalThis.fetch monkey-patch per test. Laravel's Http::fake() is the reference ergonomic here.
Proposal
A faking layer that intercepts dispatch before the network call and records what was sent, so tests can both stub responses and assert outgoing requests.
Stubbing
import { httx } from '@stacksjs/httx'
// URL-pattern → canned response
httx.fake({
'api.github.com/*': { status: 200, body: { login: 'octocat' } },
'api.stripe.com/v1/charges': { status: 402, body: { error: 'card_declined' } },
})
const res = await httx.get<{ login: string }>('https://api.github.com/users/octocat')
// res is Ok(HttxResponse) with the canned body — no network hit- A bare
httx.fake()(no arg) stubs everything with an empty200. - Pattern matching: glob (
*) on URL, or a predicate(req) => boolean. - Response shorthand:
{ status, body, headers }, a function(req) => responsefor dynamic stubs, or a sequence for "first call returns X, second returns Y":httx.fake({ 'api.test/*': httx.sequence([{ status: 500 }, { status: 200, body: ok }]) }) - Faked responses still flow through the
Result<HttxResponse<T>, Error>contract and the<T>generic, so test code reads identically to production code.
Assertions
httx.assertSent(req => req.method === 'POST' && req.url.endsWith('/charges'))
httx.assertSent(req => req.headers.Authorization === `Bearer ${token}`)
httx.assertSentCount(2)
httx.assertNothingSent()
httx.assertNotSent(req => req.url.includes('/admin'))Recorded requests reuse the existing RequestCompleteRecord shape (dist/types.d.ts:36-48) — method, url, status, headers, body — which the client already builds for onRequestComplete. The faking layer can capture those records directly.
Lifecycle
httx.fake(...)enables interception for the current process.httx.restoreFetch()(or auto-reset via a test hook) tears it down.- Unmatched requests while faking either pass through (configurable) or throw a clear "no stub matched <url>" — default to throw so tests can't accidentally hit the network.
Why
- Removes the per-test
fetchmonkey-patch boilerplate. - Makes httx testable in CI without network/credentials.
- The
RequestCompleteRecordplumbing already exists — this is mostly an intercept point + a matcher/recorder, not new request machinery.
Discovered while
Auditing the HTTP client from the Stacks side (stacksjs/stacks#1924). Sibling to the fluent-builder request (#1759).