Request context¶
The seam that lets the framework-free core build request-dependent URLs without importing a framework or holding a request.
The problem¶
A link's href often depends on the incoming request — its scheme and host, the
matched route, the current query string. But the model carrying that link is
built far from the request, down in business logic or even a pure function.
Threading the request through every layer would couple all of them to the web
framework. gazebo resolves the tension by deferring: the href is a callable,
and the request is supplied ambiently at serialization time through one small
seam.
RequestContext: the minimal surface¶
That seam is the RequestContext protocol — the minimal slice of "the request" a
link factory needs: base_url, url, query_params, and
url_for(name, **path). It's a Protocol, so anything structurally matching it
qualifies: the FastAPI glue adapts a FastAPI Request, and a test can pass a
hand-rolled object. The core only ever calls these four members.
from collections.abc import Mapping
from gazebo.context import RequestContext
class MyContext:
"""Anything structurally matching RequestContext can drive link resolution."""
@property
def base_url(self) -> str:
return 'https://api.example.com/'
@property
def url(self) -> str:
return 'https://api.example.com/plants'
@property
def query_params(self) -> Mapping[str, str]:
return {}
def url_for(self, name: str, /, **path: object) -> str:
return f'https://api.example.com/{name}'
assert isinstance(MyContext(), RequestContext) # runtime-checkable: structural match
How it's delivered¶
A context reaches a serializing model two ways, tried in order:
- The
link_contextContextVar, set by the framework glue for the duration of each request (viause_context). This is the normal path — handlers return models and the glue has already published the context. - A pydantic serialization context —
model_dump(context={'request': ctx})— for when you serialize by hand, outside any request.
If neither is present, resolving a callable href raises a clear error rather than emitting a wrong URL.
from gazebo.context import use_context
from gazebo.link import Link
link = Link.self_link()
# Under the framework glue this happens for you. To resolve manually, either bind
# the context for a block...
with use_context(MyContext()):
inside = link.model_dump_json()
# ...or pass it to model_dump as the serialization context (the test escape hatch).
outside = link.model_dump(mode='json', context={'request': MyContext()})
Manual / test resolution¶
Outside a live request — a unit test, or a script rendering a document — no
ContextVar is set, so hand the context to model_dump as above. The object only
needs to satisfy RequestContext; it doesn't have to be a real request. (This is
exactly how the examples throughout these docs stay runnable.) In a running app
under the glue, you never do this by hand.
Request id + logging (opt-in)¶
A separate nicety lives in the same module: a request_id ContextVar with
use_request_id(value) to bind one per request, and RequestIdFilter, a logging
filter that stamps each record with the active id (or - outside a request) so a
%(request_id)s format field never breaks. Wiring it into middleware is shown in
Proxy, context & health.
Reference¶
See gazebo.context.