WreathBrowse
Guides

Policy and hardening#

First-class HTTP policy is compiled with the route table. Configure the boundary once; do not scatter equivalent middleware decisions across handlers.

app.pypython
from wreath import Request, Wreath
from wreath.auth import BearerTokenBackend, Identity, authenticated
from wreath.policy import HttpPolicy, IdempotencyPolicy
from wreath.policy.cors import CorsPolicy
from wreath.policy.request_id import RequestIdPolicy
from wreath.policy.security import SecurityHeadersPolicy, TrustedHostPolicy
from wreath.response import JSONResponse

created: list[dict] = []


def verify(token: str) -> Identity | None:
    return Identity("operator") if token == "operator-token" else None


app = Wreath(
    http_policy=HttpPolicy(
        trusted_host=TrustedHostPolicy(("api.example.test",)),
        cors=CorsPolicy(allow_origins=("https://console.example.test",)),
        security_headers=SecurityHeadersPolicy(),
        request_id=RequestIdPolicy(),
        idempotency=IdempotencyPolicy(),
    )
)
app.configure_auth(BearerTokenBackend(verify))


@app.post("/deployments")
@authenticated()
async def deploy(request: Request) -> JSONResponse:
    deployment = {"id": len(created) + 1, "actor": request.identity.id}
    created.append(deployment)
    return JSONResponse(deployment, status=202)
test_app.pypython
from wreath.testing import TestClient

from app import app, created


async def test_a_retry_replays_the_first_authenticated_effect() -> None:
    created.clear()
    headers = {
        "host": "api.example.test",
        "authorization": "Bearer operator-token",
        "idempotency-key": "deploy-42",
    }
    async with TestClient(app) as client:
        first = await client.post("/deployments", headers=headers)
        retry = await client.post("/deployments", headers=headers)

    assert first.status == retry.status == 202
    assert retry.json() == first.json()
    assert dict(retry.headers)[b"idempotency-replayed"] == b"true"
    assert len(created) == 1


async def test_an_unknown_host_is_refused_before_the_handler() -> None:
    created.clear()
    async with TestClient(app) as client:
        response = await client.post(
            "/deployments",
            headers={
                "host": "attacker.example",
                "authorization": "Bearer operator-token",
                "idempotency-key": "deploy-43",
            },
        )
    assert response.status == 400
    assert created == []

Use PostgreSQL-backed rate-limit, idempotency and session stores across workers. The policy reference lists every policy and its bounded store.

The middleware catalogue#

People coming from another framework will search for “middleware”. In Wreath, the standard cases below are immutable policy values. HttpPolicy fixes their order, compiles the native parts with the route image, and lets OpenAPI describe the headers and refusals that each configured instance adds.

Familiar middleware jobHttpPolicy fieldWreath declaration
forwarded client, scheme and hostproxyProxyPolicy
allowed Host valuestrusted_hostTrustedHostPolicy
maintenance modemaintenanceMaintenancePolicy
AI crawler treatmentai_scrapingAIScrapingPolicy
classified client traffictrafficTrafficPolicy
address-level throttlingrate_limitRateLimitPolicy
authenticated plan or quota limitsprincipal_rate_limitTieredRateLimitPolicy
expiring signed URLssigned_routesSignedRoutePolicy
bounded gzip request bodiesrequest_decompressionRequestDecompressionPolicy
correlation IDsrequest_idRequestIdPolicy
Server-Timingserver_timingServerTimingPolicy
cross-origin HTTPcorsCorsPolicy
browser write protectioncsrfCsrfPolicy
CSP, HSTS and browser headerssecurity_headersSecurityHeadersPolicy
WebSocket originswebsocket_originWebSocketOriginPolicy
signed or server-side sessionssessionSessionPolicy
replay-safe unsafe requestsidempotencyIdempotencyPolicy
Cache-Control and validatorscache_controlCachePolicy
gzip, Brotli and Zstandard responsescompressionCompressionPolicy
in-flight request admissionconcurrencyConcurrencyPolicy
handler time budgetsdeadlineDeadlinePolicy

There are two rate-limit positions because they know different facts. The ingress limit runs before authentication and normally keys by client address. The principal limit runs after identity is established, so it can enforce a plan or quota. Wreath refuses a principal key in the ingress slot instead of silently putting every user in one bucket.

Application policy and route policy#

Put universal boundaries in the application's HttpPolicy. Route decorators remain the place for requirements that differ by operation: public versus authenticated, roles, permissions, Cedar authorization, second-factor freshness, quotas and an operation-specific idempotency contract. Generated OpenAPI sees both layers.

Use local memory stores only for one process. These declarations have PostgreSQL owners when several workers must agree:

DecisionOne-process ownerShared owner
idempotency replayMemoryIdempotencyStorePostgresIdempotencyStore
rate-limit bucketsMemoryRateLimitStorePostgresRateLimitStore
revocable sessionssigned cookiePostgresSessionStore
response invalidationlocal cache backendshared response_cache backend and tags

When custom middleware is actually custom#

Use wreath.middleware for application behavior that has no first-class owner. Hook middleware is compiled into a flat tape. An applies_to predicate sees static route facts at startup, so route selection is not repeated on each request.

custom_middleware.pypython
from wreath import Request, Wreath
from wreath.middleware import MiddlewareHooks, MiddlewareRoute

app = Wreath()


def is_api(route: MiddlewareRoute) -> bool:
    return route.path.startswith("/api/")


def attach_release(request: Request) -> None:
    request.state.release = "2026.08"


app.add_middleware(
    MiddlewareHooks(
        before_sync=attach_release,
        applies_to=is_api,
    )
)

Reach for legacy (request, call_next) middleware only when the handler call itself must sit inside a context manager or try/finally. One legacy middleware makes that route use nested calls instead of the compiled hook tape. Standard HTTP behavior is deliberately refused by add_middleware; configure it through HttpPolicy.

Refusals are part of the contract#

CORS preflight, rate-limit 429, maintenance 503, deadline 504, idempotency conflicts, required headers and cache validators are included in generated operation contracts. The declaration and the wire behavior come from the same configured object, so the docs cannot advertise a generic limit while production enforces another one.

esc
↑↓ navigate↵ openesc close