Skip to content

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:

  1. The link_context ContextVar, set by the framework glue for the duration of each request (via use_context). This is the normal path — handlers return models and the glue has already published the context.
  2. A pydantic serialization contextmodel_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.