Skip to main content
Fixtures are recorded HTTP exchanges that answer every request your widgets make. In fixtures mode (the default), the harness patches globalThis.fetch to refuse the network and serve only what you recorded — however your widgets fetch (react-query, SWR, an effect, or a promise read with use()).

Overview

Declare fixtures on the integration entry, keyed by variant:
The same list answers widgets and, in fixtures mode, ctx.fetch inside webhook, schedule, and action handlers. In live mode, same-origin /api/… requests are still answered from fixtures, except the active integration’s own /api/integrations/<slug>/… routes, which the harness’s server serves (see Live Mode).

Fixture variants

The toolbar’s Fixtures dropdown switches between three variants: Switching variants tears down and rebuilds the widget subtree, so any widget that re-fetches (via react-query or an effect) hits the new fixture. An omitted variant is not a fallback to default: every request under it is refused. A widget that caches its promise at MODULE scope will not; see Promise caching below.

HttpFixture

Request line

The request field is a method and a target separated by a space:
Method — case-insensitive; one of GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS. Target — one of three forms: Path matching (method and origin must be equal first):
  • Path segments must agree in count
  • :param matches exactly one path segment
  • * matches the rest (including nothing)
  • /v1/payouts does NOT match /v1/payouts/42, but /v1/payouts/:id and /v1/payouts/* both do
Query matching:
  • If your fixture has no ?, query is ignored entirely — GET https://api.acme.com/v1/data answers a request with any query string
  • If your fixture has a ?, every param in it must be present on the real request with the same value — a subset match, so you can pin two params and ignore nine others
First match wins — order matters. Write a specific fixture above a general one:

Response

The response field is your vendor’s body. If it’s already a string it’s sent as-is with text/plain; anything else is JSON-serialized with application/json.

Headers

No fixture found

When a request doesn’t match any fixture:
  1. Console line:
  2. Promise rejects with NoRecordedResponseError:
  3. Where you see the error depends on your error handling:
    • react-query lands in the error state (harness sets retry: false)
    • A widget starting a promise in render and reading it with use() throws; the per-widget error boundary renders the full message
If the URL looks unfamiliar, log it from the code that builds it. Check the path and any pinned query params for typos (trailing slash, extra segment, case mismatch).

Host routes

Same-origin /api/… requests are the host-route namespace. The harness serves no backend for them, so a fixture is what answers them, and they are refused if none matches. When a refused request was a host route, the quoted request: line is the path form (GET /api/integrations/acme/summary), because the absolute URL would pin your fixture to the harness’s port. Exceptions: the active integration’s own /api/integrations/<slug>/… routes are passed to the harness’s server in live mode, and its local-setup and actions/<name> routes are passed through in both modes.

Server-side rendering

A widget that starts its promise during render and reads it with use() also runs during the server render. Every fixture is compiled for that pass except the /api/… host-route shape. Absolute and path-only fixtures work on the server and browser identically. Host-route fixtures are not answered on the server, which has no origin to resolve them against. The request is refused with a message beginning No recorded response for <METHOD> <url> — on the SERVER render.; the client render answers it, so nothing needs to change. To answer it on the server too, make both the fixture and the widget’s URL absolute (at the cost of hardcoding the dev server’s origin). Same-origin traffic outside /api/ (/_next/static, the document, ?_rsc= payloads) is passed straight through in the browser and never intercepted. On the server render it has nowhere to go and is refused with the same — on the SERVER render. message; give it an absolute URL if it was meant to reach a vendor.

Promise caching

A widget that caches its promise at MODULE scope will not re-fetch when the fixture variant changes:
The module outlives the fixture variant switch, so it keeps serving the first variant’s answer while the toolbar says error. Solution: create the promise in the non-suspending parent:
The parent commits so its state survives; the child suspends and retries against the same promise.
This is a widget-side pattern, not a fixtures problem — the fixture swap worked correctly. Creating the promise with useState inside the component that suspends is worse: a suspended render is discarded before it commits, so the initializer runs again on every retry.

FixtureSeed (react-query fast path)

If your data layer is @tanstack/react-query, you can seed the cache directly for zero network traffic:
A FixtureSeed is { queryKey, data } or { queryKey, error }. If both are set, error wins. Rules:
  • Derive the query key from the same factory your hook calls (import it, don’t retype)
  • A seeded key never fetches, so an HTTP fixture behind it is unreachable — use one or the other
  • A query with neither a seed nor a queryFn of its own throws MissingFixtureError; there is no request to record, so seed the key
Why HTTP fixtures instead: both solve real problems, but HTTP fixtures solve two at once—you write vendor data once in one shape, serving widgets and triggers alike—and they don’t require react-query.

Credentials in fixtures

Credentials are redacted from console logs and error messages:
  • Userinfo in URLs is stripped
  • The entire query string is replaced with ?… (because an API key in a query param is exactly what ends up in screenshots)
The quoted request: line in an error therefore has no query. This is correct as written: a fixture with no ? matches whatever query your widget sends. Add ?key=value back only to pin a param you specifically want matched.

Request logging

Intercepted requests log to the console (URL redacted), which is the fastest way to see what a widget actually asked for vs. what you recorded:
  • fixture — answered from your registry
  • network — sent for real (a vendor request in live mode, or a request passed to the harness’s own routes)
  • refused — not answered: no matching fixture, or an origin outside your egress in live mode
Same-origin /api/… requests answered from fixtures log fixture or refused in either mode.

Troubleshooting

No recorded response error

Check the URL in the error message (with query redacted). Log it from the code that builds it to verify the path, params, and any hardcoded segments.

Variant switch doesn’t apply

The widget didn’t re-fetch. See Promise caching to fix it.

Widget shows old data under new variant

Same issue — the promise was created at MODULE scope. Move it into the parent component’s state.

Server render returns different data than browser

Host-route (/api/…) fixtures are not answered during the server render; the client render answers them after hydration. Make the fixture and the widget URL absolute if you need server-side data.