Validation and Re-render¶
A failing POST does not produce an error page. The dispatcher re-renders the origin page with the bound form, the cached dependencies, and a fresh CSRF token. This page explains the validation and re-render flow end to end.
The Origin Page¶
Every rendered {% form %} tag emits a hidden _next_form_page field that contains the absolute path to the current page.py.
The dispatcher reads that field on the re-render branch to locate the origin page.
On a validation failure the dispatcher rejects the submission and returns HTTP 400 Missing or invalid _next_form_page when any of the following holds.
The field is missing or blank.
The basename is not
page.py.Resolving the path raises
OSError.BASE_DIRis unset, or the resolved path falls outsideBASE_DIR.The
page.pydoes not exist on disk and no siblingtemplate.djxstands in for it.
Virtual routes are fully supported as origin pages.
A directory that has only a template.djx and no page.py is a virtual route (see File Router for the routing rules).
The {% form %} tag on such a page emits _next_form_page pointing at a non-existent page.py path.
The dispatcher accepts this: if page.py does not exist but a sibling template.djx does, the path is considered valid.
On validation failure the re-render composes the body from the template loader exactly as the initial render did, with no page module involved.
The Render Pipeline¶
A request to /_next/form/<uid>/ follows a fixed pipeline.
The dispatcher resolves the action UID to its handler and form class.
The form is constructed with POST data, uploaded files, and the initial data that
get_initialreturns.form.is_valid()runs.On valid form, the handler is called with the dependency-resolved parameters and its return value goes to the client.
On invalid form, the dispatcher loads the origin page, reattaches the cached dependencies, and re-renders the template with
formset to the bound failing form.
The pipeline stays inside the same request. A failing form does not redirect, the user stays on the same URL.
What Survives Re-render¶
One important thing carries over from the initial render.
- Dependency cache.
Read the per-request cache through
next.deps.get_request_dep_cache(request). The dispatcher stores it on the request under the attribute named byREQUEST_DEP_CACHE_ATTRso the helper can find it. The re-render reuses each cached value without rerunning the provider. A custom DI provider must therefore be idempotent across a render cycle.
Tip
A dependency shared between the action handler and a page context function resolves only once per request, so a failed validation never re-queries it. Register expensive lookups such as tenant queries and permission checks as named dependencies to get this caching for free.
The frozen FormSpec descriptors in Frozen Form Specs are a separate user-facing tool.
The dispatcher does not attach a FormSpec to the request on its own.
What Restarts on Re-render¶
Other parts of the render restart from scratch.
- Page module body source.
The
renderfunction or thetemplate.djxbody runs again. The bound form is in scope this time.- Layout chain.
Every ancestor
layout.djxrenders again. Layout level context functions still run, with the cached values where the cache hit, with fresh evaluation where it did not.- Static collector.
The collector restarts so the rendered HTML contains the right set of asset links.
The Bound Form Variable¶
On the re-rendered page the variable form is the bound form with errors.
The template can render error messages inline with each field.
On re-render the dispatcher always supplies the bound failing form under the action-named context key so the user sees the input that triggered the failure.
Override get_initial on the form class to control the initial render instead.
Multiple Forms On The Same Page¶
A page that hosts several actions only re-renders the failing form.
Other forms render as unbound, with their original @context("...") values intact.
This works because every action has its own UID and only one URL fires the re-render. The dispatcher does not rerun the validation of any other form.
Influencing the Re-render¶
One hook lets a page customise the form before it reaches the template.
- The
get_initialhook. Override
get_initialon the form class to shape the initial data or the bound model instance on the initial render. The dispatcher always supplies the bound failing form on re-render, soget_initialruns only on the initial GET.
Redirecting Back to the Origin¶
A handler that succeeds usually returns an HttpResponseRedirect.
When the action can be submitted from more than one page, hardcoding that target sends every caller to the same place.
The redirect_to_origin helper sends the user back to whichever page rendered the form.
from django.http import HttpRequest
from next.forms import action, redirect_to_origin
from next.urls import DUrl
@action("toggle_favourite")
def toggle_favourite(note_id: DUrl["id", int], request: HttpRequest):
Note.objects.filter(pk=note_id).update(favourite=True)
return redirect_to_origin(request, fallback="/notes/")
redirect_to_origin(request, fallback="/") reads the hidden _next_form_origin field that the {% form %} tag sets to request.path verbatim at render time.
It accepts the value only when it is a string that starts with a single /.
A protocol-relative input beginning with // is rejected, which blocks open-redirect input.
When the field is absent or fails validation the helper redirects to fallback instead.
Origin Versus Page Fields¶
Two hidden fields travel with every submission and serve different roles.
_next_form_page.The absolute filesystem path to the
page.pythat rendered the form. The dispatcher uses it to locate the origin module for the re-render. It is a disk path, never a URL._next_form_origin.The request path the form was rendered under, such as
/notes/42/. It is consumed only byredirect_to_originon the success path.
The re-render path uses _next_form_page. The success-redirect path uses _next_form_origin.
A failing form ignores _next_form_origin because the re-render stays on the same URL without a redirect.
Server Side Effects Before Validation¶
Side effects belong inside the handler, after form.is_valid() returns true.
The dispatcher does not call any side effect when validation fails.
This guarantee makes it safe to put database writes and external calls inside the handler.
Warning
A page level context function runs again on every re-render, so any write or external call it makes happens a second time on every validation failure. Keep context functions read only. Put writes inside the action handler where they run once on success, or in a custom provider whose result is cached on the request.
Signals¶
Two signals fire during the validation pipeline.
form_validation_failed.Fires when
form.is_valid()returns false. Payload carriesaction_name,error_count, andfield_names.action_dispatched.Fires after the handler returns successfully. Payload carries
action_name,form,url_kwargs,duration_ms,response_status, anddep_cache.
See Form Signals for the full list and payload shapes.
Edge Cases¶
Missing or stale
_next_form_pagefield returns HTTP 400. Plain HTML forms must set the field to{{ current_page_module_path }}.Origin
page.pyrenamed or deleted returns HTTP 400 when the path no longer exists on disk.Renaming the form class has no effect on the UID, which is hashed from the action name.
A handler that returns
HttpResponseRedirectskips the re-render path entirely. Use this on success only.Virtual page origins backed by
template.djxresolve through the template loader, as The Origin Page explains above.The re-render emits a fresh
csrfmiddlewaretokenthroughget_token, so the browser cookie stays valid and the resubmission passes CSRF without a reload.File inputs reset on re-render because the HTTP spec does not let a server re-populate them. Set
enctype="multipart/form-data"and re-prompt the user for the upload.Text, select, checkbox, and textarea widgets keep their raw submitted values because the dispatcher binds the failing form to
request.POST. Password widgets clear unlessrender_value=True.
Common Patterns¶
Inline Errors¶
Render {{ form.field.errors }} next to each input.
The re-render shows the previous value and the error in one place.
Cross Field Validation¶
Use Django clean and clean_<field> methods on the form class.
The dispatcher treats these failures the same as field validation failures.
Audit Trail¶
Subscribe to form_validation_failed to log every rejected attempt.
The signal fires once per failed submission so log volume scales with failure rate, not request rate.
See Also¶
See also
Actions for handler patterns.
Form Templates for the {% form %} tag and _next_form_page.
Action Backends for swapping the dispatch backend.
Action Dispatch for the full pipeline.