GazeboApp & upgrade¶
Two ways to get gazebo's machinery onto a FastAPI app: construct a
GazeboApp, orupgrade()an app you didn't create.
GazeboApp¶
GazeboApp(providers, *, overrides=None, trust=trust_none, health_path='/health',
**fastapi_kwargs) is a thin FastAPI subclass wired from a registry. It opens
the app scope in its lifespan, installs the request-scope and proxy-headers
middleware, registers the problem handlers, and mounts /health — and otherwise
is a FastAPI app, so any FastAPI(...) keyword passes straight through. It
exposes .container and .app_state for introspection. Build it behind a
create_app() factory so tests can pass
Overrides:
from gazebo.ext.fastapi import GazeboApp, GazeboRouter, Overrides, Providers
router = GazeboRouter()
@router.get('/ping')
async def ping() -> dict[str, str]:
return {'pong': 'ok'}
def create_app(overrides: Overrides | None = None) -> GazeboApp:
providers = Providers()
app = GazeboApp(providers, overrides=overrides)
app.include_router(router)
return app
upgrade() an existing app¶
When you don't construct the app — it's made by a framework, or needs custom
FastAPI(...) config you'd rather not route through a subclass — call
upgrade(app, providers, ...) to apply the same machinery in place: it wraps the
lifespan, installs the middleware, registers the handlers, rewrites @app.get
routes for injection, and adds /health. Same options as GazeboApp, and
idempotent (calling it twice is a no-op).
from fastapi import FastAPI
from gazebo.ext.fastapi import Providers, upgrade
existing = FastAPI() # someone else's app
existing.include_router(router)
upgrade(existing, Providers()) # add gazebo's machinery in place; idempotent
Problem & validation responses¶
GazeboApp and upgrade() register two exception handlers so every error
response is application/problem+json (see Problems):
problem_exception_handlerrenders anyProblemExceptionyou raise with its status and detail.validation_exception_handlermaps FastAPI'sRequestValidationErrorto a422problem, carrying the field-level errors under anerrorsextension member (RFC 9457). Without this, bad input would return FastAPI's default{"detail": [...]}shape and break problem+json uniformity.
The second is automatic — you write nothing for it:
from gazebo.ext.fastapi import GazeboApp, GazeboRouter, Providers
validating = GazeboRouter()
@validating.get('/widgets')
async def list_widgets(limit: int = 10) -> dict[str, int]:
return {'limit': limit}
vapp = GazeboApp(Providers())
vapp.include_router(validating)
# Now GET /widgets?limit=nope fails request validation and the glue returns an
# application/problem+json 422 (see the response shape below) — no handler needed.
A bad request then yields:
{
"type": "about:blank",
"title": "Unprocessable Entity",
"status": 422,
"detail": "request validation failed: 1 error(s)",
"errors": [{"type": "int_parsing", "loc": ["query", "limit"], "msg": "..."}]
}
CORS¶
OGC APIs are usually consumed from a browser, so cross-origin requests matter.
CORS is off by default (an open policy is a security smell to ship silently);
opt in with the cors= argument to GazeboApp/upgrade():
cors=True— a permissive policy (any origin, no credentials), fine for local development.cors=['https://app.example.com', ...]— restrict to an explicit origin list.cors=CorsConfig(...)— full control (methods, headers, credentials,max_age). Note thatallow_origins=['*']withallow_credentials=Trueis rejected by browsers, so credentials default off.
The middleware is installed outermost, so even a problem+json error response carries the CORS headers.
from gazebo.ext.fastapi import CorsConfig, GazeboApp, Providers
# cors=True is permissive (allow all origins) — handy for local development.
dev_app = GazeboApp(Providers(), cors=True)
# A list restricts to specific origins; a CorsConfig gives full control (here,
# allowing credentialed requests, which `*` cannot).
prod_app = GazeboApp(
Providers(),
cors=CorsConfig(
allow_origins=['https://app.example.com'],
allow_credentials=True,
),
)
Link: response header¶
The same navigational links gazebo resolves into a JSON body can also be emitted as
an RFC 8288 Link: header, so crawlers and
non-JSON clients can follow self/next/prev/alternate without parsing the body.
Call set_link_header inside an
endpoint with the links you're returning — it's the companion to
set_cache_headers, stamping a
header onto the injected Response.
It takes any sequence of Link (a collection's
.links, or a hand-built list) and resolves the deferred hrefs against the active
request. By default only navigational rels
(NAV_RELS) are emitted, capped at
max_links, so a large collection can't produce an oversized header; pass rels= to
narrow further (e.g. rels=['self', 'next', 'prev']) or rels=None for every rel.
from fastapi import Response
from gazebo.collection import LinkedCollection
from gazebo.ext.fastapi import GazeboApp, Providers, set_link_header
from gazebo.link import Link
hdr_app = GazeboApp(Providers())
class Items(LinkedCollection[dict], items_alias='items'):
pass
@hdr_app.get('/items', response_model=Items)
async def list_items(response: Response) -> Items:
links = [Link.self_link()]
# Mirror the navigational links into an RFC 8288 Link: header. Pass a rel list
# (e.g. rels=['self', 'next', 'prev']) to narrow it further.
set_link_header(response, links)
return Items(items=[{'id': 1}], links=links)
Mounting under a root app¶
A mounted sub-app's lifespan isn't run automatically, so a mounted GazeboApp
would never open its app scope. Set the root app's lifespan to
forward_lifespans(sub_app) to run it. This is general framework behavior, not
gazebo-specific — but it's the one wiring step that's easy to miss.
from fastapi import FastAPI
from gazebo.ext.fastapi import forward_lifespans
sub = create_app() # a GazeboApp
root = FastAPI(lifespan=forward_lifespans(sub)) # run the sub-app's lifespan
root.mount('/api', sub)
Reference¶
See gazebo.ext.fastapi
(GazeboApp, upgrade, forward_lifespans).