next.partial API reference#
Module summary#
next.partial exposes the server side of partial rendering.
The surface covers the Patches builder that authors a patch envelope, the response and stream classes that carry it, and the zone-render and origin helpers.
It also covers the custom-verb registration hook and the protocol backend that serialises the wire format.
The wire protocol, the data-next-* attributes, and the client runtime live in the topic section, see Partial rendering reference.
API tiers#
The surface splits into tiers that describe the intended audience for each name. The lists below are representative. The autodoc blocks under Public API are the exhaustive surface.
- Stable.
Patches,PatchResponse,PatchEventStream,render_zone,ZoneRenderResult,zone_requested,is_partial_request,partial_intent,register_patch_op,UnknownZoneError,ForeignPageNotAuthorizedError, andLayerHrefWithoutZoneError.Envelope,Patch,Asset, andFormMetaare the frozen value objects of the wire contract. Import all of these fromnext.partial. Use them in page modules, action handlers, and stream sources.- Advanced.
shape_partialandPartialProtocolBackendare imported fromnext.partial.resolve_partial_originstays innext.partialas a thin helper that reads the host page out of theX-Next-Originheader so adonestep can pass it tomorph(page=).OriginSourcelives innext.partial.origin.ZoneInfoandzones_oflive innext.partial.registry. The custom-verb exceptions live innext.partial.patches. Thesignalsandcheckssubmodules carry the partial telemetry. Use these when writing a custom protocol backend, a wire-format plugin, or telemetry.- Framework machinery.
REQUEST_IDis theX-Next-Request-Idheader name and lives innext.partial.headers.PartialIntentandMergeModelive innext.partial.headers.PartialOriginlives innext.partial.origin.ActionRef,shape_validate, anddrain_messageslive innext.partial.shaping.PatchOpRegistry, thepatch_op_registryinstance, andBUILTIN_OPSlive innext.partial.registry.- Internal hooks.
Underscore-prefixed helpers inside the submodules are implementation details.
next.partial.__all__is the source of truth for the curated package surface and exports no underscore names.
Public API#
Detecting a partial request#
is_partial_request returns True when the request carries the X-Next-Request
switch, the test a render escape hatch reads before shaping a patch response.
partial_intent parses and memoises the X-Next-* headers into a PartialIntent.
zone_requested answers whether the intent names a given zone, the guard a lazy zone’s
context provider reads to skip an expensive query on a full render.
- next.partial.is_partial_request(request: HttpRequest) bool[source]#
Return True when the request asks for a partial response.
- next.partial.partial_intent(request: HttpRequest) PartialIntent[source]#
Return the partial intent of the request, memoised on the request.
- next.partial.zone_requested(request: HttpRequest, name: str) bool[source]#
Return True when the partial intent of the request names the zone.
- class next.partial.headers.PartialIntent(partial: bool = False, zones: tuple[str, ...] = (), validate_fields: tuple[str, ...] = (), merge: MergeMode | None = None, version: str | None = None, request_id: str | None = None, origin: str | None = None)[source]#
Parsed partial-request headers naming what the client asks for.
The fields mirror the request-header table of the wire protocol. A request without the X-Next-Request switch is not partial and every derived field stays empty. Names are server-registry indices, never selectors or swap strategies.
Building patches#
Patches is the request-bound builder.
Each method records one operation and returns self for chaining, and response
finalises a PatchResponse or falls back to a redirect when the runtime is absent.
Envelope, Patch, Asset, and FormMeta are the frozen value objects the
builder assembles, surfaced for a custom backend that serialises the wire format itself.
- class next.partial.Patches(request: HttpRequest, *, echo_of: str | None = None)[source]#
Request-bound builder of a patch envelope.
Built from a request, the builder takes its asset version from the active protocol backend and resolves the origin page lazily, so a morph(zone=…) renders against the page that owns the request. The versioned classmethod builds a request-free assembler for paths that already hold the version and render their own HTML.
- __init__(request: HttpRequest, *, echo_of: str | None = None) None[source]#
Start an empty builder bound to the request.
Pass echo_of with the originating mutation’s request id so the envelope carries it as request_id, letting an SSE subscriber suppress its own echo. Only the stream path passes it, the HTTP response path leaves it unset since the answer already reaches the initiator.
- classmethod versioned(version: str, *, echo_of: str | None = None) Patches[source]#
Start an empty request-free builder stamped with a literal version.
The builder stays a low-level envelope assembler with no request, used by paths that already hold the version and render their own HTML.
- morph(target: Mapping[str, Any] | None = None, html: str | None = None, *, extract: bool = False, **select) Patches[source]#
Morph a target into HTML, the default verb.
A thin facade over the typed per-verb morph methods that keeps the single-verb mental model. The facade routes the selector keyword to the method that owns its contract, so two selectors in one call or an unknown selector raises rather than being silently dropped.
- morph_zone(zone: str, *, overrides: Mapping[str, Any] | None = None) Patches[source]#
Render the named zone of the origin page and morph it in place.
- morph_foreign_zone(zone: str, page: Path | str, *, url_kwargs: Mapping[str, Any] | None = None) Patches[source]#
Render a zone of a foreign page out of band, re-running its guards.
The page is named by its page path or by a URL of it, which is resolved through the URLconf to the page that serves it. The foreign page’s body resolution runs first, so a redirect or a denial short-circuits before any zone renders and raises instead of morphing an empty body. A render() string body has no zone to render standalone, so it is refused the same way the OOB view branch refuses it. With the page authorized, the named zone renders standalone with the foreign page’s URL kwargs and morphs in place addressed by zone name.
- morph_form(uid: str, html: str) Patches[source]#
Extract-morph the form addressed by its uid into the given HTML.
- replace(target: Mapping[str, Any], html: str) Patches[source]#
Replace the target node wholesale with the given HTML.
- inner(target: Mapping[str, Any], html: str) Patches[source]#
Replace only the contents of the target with the given HTML.
- append(target: Mapping[str, Any], html: str, *, dedupe: Literal['key', 'id'] = 'key') Patches[source]#
Append children to the target, deduplicating by key or id.
- prepend(target: Mapping[str, Any], html: str, *, dedupe: Literal['key', 'id'] = 'key') Patches[source]#
Prepend children to the target, deduplicating by key or id.
- refresh(*, zone: str) Patches[source]#
Ask the client to refetch the named zone with its own cookies.
- context(**names) Patches[source]#
Merge named serialize provider values into the client context.
Only the names of registered serialize=True providers on the origin page are accepted. A framework-owned init-payload key raises ReservedContextKeyError whether or not the origin page registered it, so the refusal does not depend on the collision the check warns about. The values are serialized through resolve_serializer() so the wire carries plain data.
- layer_open(*, zone: str | None = None, href: str | None = None) Patches[source]#
Open a server-initiated layer, optionally seeding a zone or href.
A seeded href needs a zone to load into and must be same-site, so a href without a zone raises LayerHrefWithoutZoneError and a cross-site value raises CrossSiteHrefError.
- layer_close(*, result: object = None, dismiss: str | None = None) Patches[source]#
Close the top layer with an accept result or a dismissal.
A dismissal sets the boolean dismiss flag the client reads and carries the reason string under reason, matching the wire shape the runtime expects rather than overloading dismiss with the text.
- toast(text: str, variant: str = 'info') Patches[source]#
Show a toast, sugar over an event with a built-in container.
- event(name: str, detail: Mapping[str, Any] | None = None) Patches[source]#
Dispatch a CustomEvent on document and the Next.on bus.
A framework-owned event name raises ReservedEventNameError so an app cannot forge a runtime lifecycle event.
- push_url(href: str) Patches[source]#
Push the validated href onto the browser history.
The href must be same-site, a cross-site value raises CrossSiteHrefError rather than being masked as the origin path.
- redirect(href: str, *, external: bool = False) Patches[source]#
Drive a full client navigation to a server-authored href.
An internal href must be same-site, a cross-site value raises CrossSiteHrefError. An external href is sent with a full-navigation marker so a server-authored redirect like OAuth or a payment gateway is not rejected by the same-host validator.
The external=True escape hatch bypasses same-host validation, so the href must be server authored. Never pass user-supplied input through it, or the page becomes an open redirect.
- op(name: str, **payload) Patches[source]#
Emit a custom verb registered through register_patch_op.
A built-in verb is refused so it travels only through its typed method, which owns the verb’s wire keys, never as a raw payload.
- add_asset(kind: str, url: str, *, inline: str | None = None) Patches[source]#
Record a co-located asset in the envelope manifest.
The insertion verb comes from the kind registry, so an unregistered kind still travels and only loses the field the runtime would use. An inline body keeps the verb only when the runtime builds the same element the full page render wraps it in.
- set_csrf(csrf: Mapping[str, Any]) Patches[source]#
Attach the rotated CSRF payload so the runtime refreshes tokens.
- response(fallback: str | None = None) PatchResponse | HttpResponse[source]#
Assemble the response for the current request.
With the partial switch on the request the envelope travels as a PatchResponse. Without the switch, mutation falls back to the full cycle: a 303 to the request origin when no fallback is given, or a redirect to fallback when it is.
- class next.partial.PatchResponse(body: bytes, *, content_type: str = 'application/vnd.next.patches+json', version: str | None = None, status: int = 200)[source]#
HTTP response that carries a serialized patch envelope.
The response is an HttpResponse subclass so it passes the handler normalisation contract that requires rich return types to subclass HttpResponse. The body bytes and content type come from the active protocol backend, the partial Vary headers are set on construction.
- class next.partial.Envelope(version: str, ops: Sequence[Patch] = (), assets: Sequence[Asset] = (), form: FormMeta | None = None, csrf: Mapping[str, Any] | None = None, request_id: str | None = None)[source]#
A patch envelope carrying ordered ops and protocol meta.
Every field but version is optional, an absent value is empty on the wire. The csrf and request_id meta are stamped only when set so the wire shape stays stable whether or not they travel.
- class next.partial.Patch(op: str, target: Mapping[str, Any] | None=None, html: str | None = None, extras: Mapping[str, Any]=<factory>)[source]#
One addressed DOM operation of a patch envelope.
A patch carries a verb, an optional target object with a single key, optional HTML payload, and any verb-specific extras.
- class next.partial.Asset(kind: str, url: str, inline: str | None = None, load: str | None = None)[source]#
One co-located asset of a rendered target by kind, URL, and inline body.
The load field is the client insertion verb resolved from the kind registry. It stays None for a kind the runtime cannot insert, and the wire then omits the field entirely.
Custom verbs#
register_patch_op registers a custom verb name on the server, validated by the
next.E066 check, and earns the generic Patches.op channel.
An unregistered name fails at runtime with UnknownPatchOpError.
The client supplies the handler through Next.partial.defineOp.
See Extending the protocol for the end-to-end recipe.
Zones#
render_zone renders one or more zones of a page standalone with the full page context,
returning a ZoneRenderResult that carries the wrapped HTML and the collected assets.
zones_of returns the compiled zones of a template as a mapping of ZoneInfo, both
reached through next.partial.registry.
- next.partial.render_zone(page_path: Path, zone_names: tuple[str, ...], request: HttpRequest, url_kwargs: dict[str, object] | None = None, overrides: dict[str, object] | None = None, *, context_data: dict[str, object] | None = None) ZoneRenderResult[source]#
Render the named zones of a page with the full page context.
The context is collected once for the whole batch of names through build_render_context and a fresh collector is seeded by the same convention as the canonical render path, so co-located assets of the zone bodies are gathered. A caller that already built the origin context passes it as context_data so it is reused rather than rebuilt. The manifest travels outward in the result rather than through inject, which is a no-op for fragments. Unknown zone names are skipped so one stale name never poisons a batch, but a batch of only unknown names raises so a single-zone request keeps its 400.
- class next.partial.ZoneRenderResult(html: dict[str, str], bodies: dict[str, str], collector: StaticCollector)[source]#
Rendered zones plus the assets their bodies collected.
html maps each rendered zone name to its wrapped marker element and bodies maps it to the bare inner body. A morph or replace addresses the wrapped element, an append or prepend grafts the bare body into the live zone. collector carries the co-located assets the bodies registered so the caller can ship a manifest outward, past the no-op inject.
- collector: StaticCollector#
- url_assets() Iterator[tuple[str, str]][source]#
Yield the (kind, url) pair of each collected asset that has a URL.
- inline_assets() Iterator[tuple[str, str]][source]#
Yield the (kind, inline body) of each collected inline asset.
- class next.partial.registry.ZoneInfo(name: str, partial: ZonePartial, options: ZoneOptions)[source]#
One compiled zone of a composed page template.
The render paths consume options whole, and the scalar read surface delegates to it so no mode can drift between the two.
- partial: ZonePartial#
- options: ZoneOptions#
- next.partial.registry.zones_of(template: Template) Mapping[str, ZoneInfo][source]#
Return the named zones of a compiled template, memoised per object.
The cache keys on the compiled template object, so a recompiled page gets a fresh entry while the stale object is collected. The first read of a template announces its zones through zone_registered.
Shaping#
shape_partial turns a form action outcome into a patch envelope, the body of the
bundled form backend’s partial-aware shape_response.
A custom backend that overrides shape_response routes partial requests through it,
or the next.W068 check warns that the runtime receives a full page instead.
- next.partial.shape_partial(backend: FormActionBackend, request: HttpRequest, outcome: ActionOutcome) HttpResponse[source]#
Shape one action outcome as a patch envelope for a partial request.
The CSRF rotation marker is read here, before any form or zone re-render mints a token and sets the marker as a side effect, so a login on the submit path stamps the fresh token onto whichever shape the outcome takes. Reading it after a re-render would flag every response as rotated.
SSE stream#
PatchEventStream is a StreamingHttpResponse that serialises each
Patches from a sync or async source as one next-patches event.
An async source requires ASGI and a sync source requires WSGI, and a mismatch raises
ImproperlyConfigured when the response is built.
See SSE under WSGI and ASGI for the WSGI and ASGI contract.
- class next.partial.PatchEventStream(request: HttpRequest, source: Iterable[Patches] | AsyncIterable[Patches], *, heartbeat_seconds: float | None = None, clock: Callable[[], float] | None = None)[source]#
SSE response that emits patch envelopes as next-patches events.
Each envelope yielded by the source travels as one next-patches event serialized by the active protocol backend, the same shape an HTTP partial response carries. A sync source streams envelopes as they arrive with no heartbeat, a blocked next() having nothing to interrupt it without a thread. An async source under ASGI interleaves heartbeat comments during quiet periods through asyncio.wait. The politeness headers and the leading retry hint are set on construction so a buffering proxy or GZipMiddleware does not eat the flush. The sse_stream_opened signal fires on construction and sse_stream_closed fires when the stream ends.
The source kind must match the server kind. Django buffers an async iterator fully under WSGI and a sync iterator fully under ASGI before the first byte, which hangs an infinite stream, so the constructor raises ImproperlyConfigured when an async source meets a WSGI request or a sync source meets an ASGI request.
- __init__(request: HttpRequest, source: Iterable[Patches] | AsyncIterable[Patches], *, heartbeat_seconds: float | None = None, clock: Callable[[], float] | None = None) None[source]#
Build the stream over a sync or async source of patch builders.
The heartbeat interval falls back to the active backend’s HEARTBEAT_SECONDS option when no explicit argument is passed.
Protocol backend#
PartialProtocolBackend owns the patch wire format and is the first entry of
PARTIAL_BACKENDS.
Subclass it and serialise a different envelope shape to support another wire format.
- class next.partial.PartialProtocolBackend(config: Mapping[str, Any] | None = None)[source]#
Owner of the wire format for patch envelopes.
The default backend serialises envelopes as a compact JSON envelope under application/vnd.next.patches+json. A third party may swap the wire format, for example to emulate Turbo Streams, by registering a different backend through PARTIAL_BACKENDS without touching shaping or the registries. Both serialize_envelope and sse_event operate over the same JSON envelope.
- __init__(config: Mapping[str, Any] | None = None) None[source]#
Store the merged backend config and its options.
Exceptions#
UnknownZoneError, ForeignPageNotAuthorizedError, and LayerHrefWithoutZoneError
are curated next.partial exceptions.
UnknownZoneError is raised when a partial request names a zone the template does not
declare, surfacing as a 400 before any render.
ForeignPageNotAuthorizedError is raised when an out-of-band morph of a foreign page
fails that page’s own authorisation, so a zone never travels in a response the page would
have denied.
LayerHrefWithoutZoneError is raised when a layer seeds an href but names no
zone= to load it into, so the builder refuses the layer instead of opening an empty
one on the client.
The remaining nine are rarely caught and stay out of the curated surface.
They guard the custom-verb contract, the event-name, context-key, and dedupe vocabularies,
and the foreign-page and href rules, and live in next.partial.patches.
- exception next.partial.UnknownZoneError(zone_name: str, declared: tuple[str, ...] = ())[source]#
Raised when a partial request names a zone the page does not declare.
The unified view turns this into a 400 before any zone renders, so a typo or a stale client never trips a partial render. The message names the declared zones so a builder-path typo points at what is available.
- exception next.partial.ForeignPageNotAuthorizedError(page_path: Path, status_code: int)[source]#
Raised when an OOB morph names a foreign page that denies the request.
A morph(page=…) re-runs the foreign page’s body resolution before rendering its zone, so the zone never travels in the master’s response when the page would have redirected or denied the caller on its own request. The denial is surfaced rather than swallowed into an empty morph so the master path can answer with a clear shape.
- exception next.partial.LayerHrefWithoutZoneError(href: str)[source]#
Raised when a layer seeds an href but names no zone to load it into.
The client fetch path needs a zone to know which fragment of the href to pull, so an href without one would silently open an empty modal, which is a caller bug refused at the builder.
- exception next.partial.patches.UnknownPatchOpError(name: str)[source]#
Raised when the builder is asked to emit an unregistered verb.
The runtime guard pairs with the next.E066 check, so an unknown verb fails fast in op() rather than reaching the client.
- exception next.partial.patches.BuiltinPatchOpError(name: str)[source]#
Raised when the generic op() channel names a built-in verb.
A built-in verb owns typed wire keys, so it must travel through its typed builder method rather than the raw op() payload channel.
- exception next.partial.patches.ReservedPatchKeyError(op: str, reserved: frozenset[str])[source]#
Raised when a custom op payload names a structural wire key.
The op, target, and html keys carry the patch structure, so a payload that names one of them is refused rather than overwriting it.
- exception next.partial.patches.UnknownContextNameError(name: str, available: tuple[str, ...] = ())[source]#
Raised when context() names a value that is not a serialize provider.
Only the names of registered serialize=True context providers may travel in a context patch, so an arbitrary mapping is rejected at the builder rather than serialized blind. The message names the available providers so a typo points at what is registered.
- exception next.partial.patches.ReservedContextKeyError(reserved: frozenset[str])[source]#
Raised when context() names a key the init payload owns.
A full render keeps a reserved key for the framework, so a context patch that names one would leave the client store disagreeing with the page it patches. The explicit naming is a caller bug refused at the builder rather than merged on the client.
- exception next.partial.patches.ReservedEventNameError(name: str)[source]#
Raised when event() names a framework-owned client-bus event.
The ready and context-updated events and the partial: and next: prefixes belong to the runtime lifecycle, so an app event under one of those names is refused rather than forging a framework signal.
- exception next.partial.patches.DynamicForeignPageError(page_path: Path)[source]#
Raised when an OOB morph names a foreign page with a render() body.
A render() string body never reaches the composed-template cache, so it has no compiled source to render a standalone zone against. The OOB view branch refuses the same shape with a 400, so the builder refuses it here rather than morphing the page’s stale static template.
- exception next.partial.patches.UnknownDedupeError(dedupe: str)[source]#
Raised when a merge op names a dedupe strategy the client cannot apply.
The client keys a merge row by data-next-key then id, so only key and id mean anything on the wire, an unknown value is refused at the builder rather than dropped to a silent no-dedup downstream.
- exception next.partial.patches.CrossSiteHrefError(href: str)[source]#
Raised when a builder href sink names a cross-site URL.
The push_url, layer_open(href=), and internal redirect sinks author an in-app navigation, so a cross-site href is a caller bug refused at the builder rather than masked as a fallback to the origin path. A server-authored external destination travels through redirect(external=True) instead.
Signals#
See Signals reference and Signals for the partial signals
(zone_registered, zone_rendered, patch_op_registered, field_validated,
sse_stream_opened, sse_stream_closed).
System checks#
See System checks for the zone-placement, template-compile, custom-verb, and
backend-configuration checks
(next.E060 through next.E067, next.E072, next.E073,
next.W067 through next.W071).
See also#
See also
Partial rendering for the topic subtree.
Partial rendering reference for the wire protocol and client runtime.
Extending the protocol for custom verbs and server-pushed context.
Settings for PARTIAL_BACKENDS.