Skip to content

ASGI/FastAPI Middleware

Use this tutorial when your app is a long-running ASGI service and you do not want to add handle.capture(...) inside every endpoint.

ReplayLab still works the same way:

  1. initialize the SDK once at app startup
  2. keep normal provider code in your endpoint
  3. call replaylab.instrument_app(app, handle=handle)
  4. replay the captured provider capsule locally

The middleware opens one request-scoped capture for HTTP requests that call supported providers. Health checks and provider-free requests do not write capsules by default.

Setup

From the repo checkout, install the development environment:

uv sync --all-packages --all-groups

For a no-network proof of the workflow, run the maintained scenario:

python scripts/run_scenario.py run auto-instrumentation-local --keep-workspace

Expected ending:

ReplayLab scenario passed.
Scenario: auto-instrumentation-local
Tier: loopback
Boundaries: 1
Payloads: 2
Providers: requests

This means ReplayLab captured one provider boundary during a FastAPI request, stopped the provider server, replayed the same app flow without that provider, compared the report, generated pytest, and ran the generated test.

For a richer maintainer check of the same adapter, run:

python scripts/run_scenario.py run asgi-lifecycle-local --keep-workspace

That scenario sends an ignored /health request, a provider-free /ready request, and a provider-backed /tickets/123 request carrying request ID, authorization, and cookie headers. It expects only the provider-backed request to write a capsule, and it verifies that only the configured request ID is stored.

Add Middleware

In your app, initialize ReplayLab near startup and add the middleware:

from fastapi import FastAPI
import replaylab
from replaylab import CapturePayloadPolicy

handle = replaylab.init(
    project_name="support-bot",
    auto_patch_integrations="auto",
    capture_payload_policy=CapturePayloadPolicy.FULL,
)

app = FastAPI()
replaylab.instrument_app(app, handle=handle, ignored_paths=("/health",))

"auto" enables all supported provider patchers. In production you can narrow the patch surface with an explicit tuple such as ("requests",). Direct ReplayLabASGIMiddleware registration remains supported when you want explicit framework configuration.

Your endpoint code stays normal:

import requests


@app.get("/tickets/{ticket_id}")
async def classify_ticket(ticket_id: str) -> dict[str, str]:
    response = requests.get(
        f"https://support.example.test/tickets/{ticket_id}",
        timeout=5,
    )
    response.raise_for_status()
    return {"ticket_id": ticket_id, "provider": "requests"}

You do not call handle.capture(...) in the endpoint. The middleware creates the request capture scope and provider wrappers attach boundaries to it.

Inspect The Capsule

After a request that calls a supported provider, list capsules:

uv run replaylab capsule list --local-store-root .replaylab

Look for a capsule with integrations like:

requests, asgi, auto_patch, same_process

Inspect it:

uv run replaylab capsule inspect <capsule_id> --local-store-root .replaylab

You should see one HTTP boundary. The capsule includes safe framework metadata such as request method, path, best-effort route path, endpoint name, status code, and optional request ID. ReplayLab does not store framework request bodies, response bodies, cookies, or authorization header values from the ASGI layer.

Replay

Replay runs the same app command under ReplayLab's replay runtime:

uv run replaylab replay <capsule_id> \
  --local-store-root .replaylab \
  --auto-patch-integrations auto \
  --report-id replay_fastapi_request \
  -- python app.py

Compare the report:

uv run replaylab report compare \
  <capsule_id> \
  .replaylab/replays/replay_fastapi_request/report.json \
  --local-store-root .replaylab

Expected result:

Status: succeeded
Boundaries: expected=1, replayed=1, problems=0

This means the provider call was served from the capsule rather than from the live dependency.

Generate A Regression

Generate a pytest provider replay guard from the capsule:

uv run replaylab generate-test <capsule_id> \
  --output tests/regression/test_fastapi_request_replay.py \
  --fixture-root tests/fixtures/replaylab/capsules \
  --app-root . \
  --auto-patch-integrations auto \
  -- python app.py

Run it:

uv run pytest tests/regression/test_fastapi_request_replay.py

The generated test runs your app command through replaylab replay, asserts the replay report, and fails if the request no longer matches the captured provider boundary.

Current Boundaries

  • HTTP requests only. Lifespan and websocket scopes pass through unchanged.
  • Provider-free requests do not write capsules unless write_empty_captures=True.
  • Framework request/response bodies are not captured by the middleware.
  • Replay still uses replaylab replay for local regression execution.
  • Celery, worker middleware, cloud upload, and hosted issue grouping are future work.