Skip to content

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 Accept header. RequestContext is 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:

  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 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.