Skip to content

Proxy, context & health

Make generated URLs correct behind a load balancer, and surface readiness — the operational edges of a gazebo app.

Proxy-aware URLs

Behind a TLS-terminating load balancer the app sees plain http and an internal host, so generated URLs come out wrong. ProxyHeadersMiddleware reads X-Forwarded-Proto, -Host, and -Prefix and applies them to the ASGI scope, so ctx.url, url_for, and every deferred link come out with the right scheme, host, and root path. It supersedes uvicorn's --proxy-headers (which doesn't set the scheme) and is pure ASGI, so it works under any ASGI framework. GazeboApp installs it for you.

Trust policies

Forwarded headers are client-supplied and trivially spoofed, so they're applied only when the request is trusted — and the default is to trust nothing. Pick a policy and pass it as GazeboApp(..., trust=...):

  • trust_all / trust_none — the extremes;
  • TrustedClient(*hosts) — trust an allowlist of immediate client hosts (loopback included by default);
  • SharedSecret(secret, header=...) — trust requests carrying a matching secret header (proxy-chain auth);
  • all_of(...) / any_of(...) — combine policies, e.g. require both a known host and the secret.
import os

from gazebo.asgi import SharedSecret, TrustedClient, all_of
from gazebo.ext.fastapi import GazeboApp, Providers

# Only honor forwarded headers when the request is both from a known proxy host
# and carries the shared secret (defense in depth). Default is to trust nothing.
trust = all_of(
    TrustedClient('10.0.0.1'),
    SharedSecret(os.environ.get('PROXY_SECRET', 'shh')),
)
app = GazeboApp(Providers(), trust=trust)

Request context middleware (no GazeboApp)

GazeboApp publishes the link RequestContext for you via its request scope. If you're running gazebo's models under a different ASGI stack, ContextMiddleware(app, factory) does just that one job: it builds a RequestContext from the ASGI scope (your factory) and binds it for the request. This is the escape hatch for using deferred links without GazeboApp.

Request id & logging

Pair a small middleware that calls use_request_id(...) with the RequestIdFilter on your log handler, and every log line for a request is tagged with its id. It's opt-in — gazebo doesn't impose a logging config:

import uuid

from gazebo.asgi import ASGIApp, Receive, Scope, Send
from gazebo.context import RequestIdFilter, use_request_id


class RequestIdMiddleware:
    """Pure-ASGI middleware that tags each request with an id."""

    def __init__(self, app: ASGIApp) -> None:
        self.app = app

    async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
        if scope['type'] != 'http':
            await self.app(scope, receive, send)
            return
        with use_request_id(str(uuid.uuid4())):
            await self.app(scope, receive, send)


# Reference %(request_id)s in your log format; the filter supplies the value.
handler = logging.StreamHandler()
handler.addFilter(RequestIdFilter())
handler.setFormatter(logging.Formatter('%(request_id)s %(message)s'))

Health

GazeboApp mounts GET /health (rename via health_path=, or None to disable). It probes each app-scoped resource exposing a __health__() and returns a per-resource and aggregate status — a readiness check assembled from the resources you already built, with nothing extra to maintain.

Reference

See gazebo.asgi and gazebo.context.