Request context¶
The core never imports a web framework, yet some core features depend on the live request — link URLs on its scheme/host/route, content negotiation on its
Acceptheader.RequestContextis the seam that squares that — the request-metadata surface the core needs, delivered ambiently.
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 (Why gazebo shows
that coupling in code). gazebo resolves the tension by deferring: the href is
a callable, and the request is supplied ambiently at serialization time, through
the one small seam this page describes.
RequestContext: the request-metadata surface¶
That seam is the RequestContext
protocol — the slice of "the request" the core's request-derived features need. For
URL building: base_url, url, query_params, url_for(name, **path), and
url_for_template (which resolves a route while leaving selected variables as RFC
6570 {var} expressions, backing templated links). For
content negotiation: headers, the request headers as a case-insensitive mapping,
so core negotiation can fall back to the ambient request's Accept
(Content negotiation covers that). 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. Because the protocol is @runtime_checkable, a
conforming object must implement the whole surface.
from collections.abc import Mapping, Sequence
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 {}
@property
def headers(self) -> Mapping[str, str]:
return {}
def url_for(self, name: str, /, **path: object) -> str:
return f'https://api.example.com/{name}'
def url_for_template(
self,
name: str,
path: Mapping[str, object],
template: Sequence[str],
/,
) -> str:
vars = '/'.join(f'{{{v}}}' for v in template)
return f'https://api.example.com/{name}/{vars}'
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 sibling module gazebo.requestid: 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. It's
independent of the link context above. Wiring it into middleware is shown in
Proxy, context & health.
Reference¶
See gazebo.context and
gazebo.requestid.