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.