Content negotiation¶
Pick a representation from
?f=(thenAccept), and link the alternates — without gazebo taking a position on HTML or templating.
OGC clients live on ?f=json|html, with the HTTP Accept header as the fallback. The
core ships the ALTERNATE rel and an HTML media type but no negotiation logic, so
choosing a representation — and advertising the others — was on you. gazebo.negotiation
closes that gap with pure resolution: given the representations a resource offers, it
picks one and builds the alternate links to the rest. It deliberately ships no HTML
renderer: turning the chosen representation into bytes (a template, a callable) is the
app's job; gazebo only tells you which representation and links the others.
Resolving a representation¶
A Representation pairs a ?f= key
with a media type (JSON, GEOJSON, HTML are ready-made). negotiate() applies the
OGC order — ?f= wins, then Accept, then the first offered (or an explicit
default):
from gazebo.negotiation import HTML, JSON, negotiate
# ?f= wins; otherwise the Accept header; otherwise the first offered representation.
assert negotiate([JSON, HTML], f='html') is HTML
assert negotiate([JSON, HTML], accept='text/html;q=0.9, application/json;q=0.1') is HTML
assert negotiate([JSON, HTML]) is JSON
A ?f= naming a format that isn't offered is a client error
(ParamError → 400); an Accept that lists nothing on offer is a 406
(ProblemException). Both already render as problem+json through the
FastAPI glue, so a failed negotiation needs no extra handler.
In a route¶
The glue's Negotiate([...]) dependency resolves the representation from the request
(?f= query + Accept header). The endpoint branches on it — render HTML or return the
model — and attaches alternate_links() so each representation advertises the others.
Inject Response semantics by returning an HTMLResponse for the HTML branch while
keeping a response_model for the JSON one:
from gazebo import Link, OmitNullModel
from gazebo.ext.fastapi import GazeboApp, Negotiate, Providers
from gazebo.negotiation import HTML, JSON, Representation, alternate_links
app = GazeboApp(Providers())
class Doc(OmitNullModel):
id: str
links: list[Link] = Field(default_factory=list)
@app.get('/collections/{cid}', response_model=Doc)
async def collection(
cid: str,
rep: Annotated[Representation, Negotiate([JSON, HTML])],
) -> Doc | Response:
# self for the current representation, alternate links to the others
links = [Link.self_link(type=rep.media_type), *alternate_links(rep, [JSON, HTML])]
if rep.key == 'html':
return HTMLResponse(f'<h1>{cid}</h1>')
return Doc(id=cid, links=links)
alternate_links(current, available) returns one deferred alternate link per other
representation, each pointing at the current URL with ?f= switched — so a client on
the JSON view can discover and follow the HTML one. Pair it with a normal self link
for the current representation.
Reference¶
See gazebo.negotiation (Representation,
negotiate, alternate_links) and the glue's
Negotiate dependency.