Skip to content
AITroveRead. Build. Understand.
Make this comfortable

Django Forms, CSRF, Atomic Approval, and On-Commit Work

Last updated: 5 Oct 20268 min read
tutorial
IntermediateBy AITrove Editorial

A browser form that mutates state crosses several independent boundaries. CSRF protection checks that a same-origin user action is not silently forged through the browser's ambient credentials. Form validation checks the submitted fields. Authorization checks the current reviewer and case relationship. A database transaction protects the state transition from partial writes. None of these substitutes for the others. A retry after a lost response can still repeat an already committed action unless the command carries a stable idempotency key stored under a unique constraint. Notification dispatch belongs after the transaction commits, because a sent notification for rolled-back work is a lie.

Working case

Reviewer 47 approves case 62 through a POST form. The browser times out after the database commit and the reviewer presses Submit again. A naive handler adds a second audit entry and sends another notification. In a separate failure, the notification queue accepts a message before a later validation error rolls back the approval. The corrected handler requires the page's CSRF token, validates the operation key and requested transition, locks and authorizes the case inside one transaction, and records the operation under a uniqueness rule. It schedules a notification only for a newly created approval and only after a successful commit. A replay returns the saved outcome.

Implementation boundary

python
from django.db import transaction
from django.http import Http404, JsonResponse
from django.views.decorators.http import require_POST
from django.contrib.auth.decorators import login_required
from .services import approve_once, wake_approval_outbox


@require_POST
@login_required
def approve_permit(request, case_id: int):
    operation_id = request.POST.get("operation_id", "")
    if not operation_id or len(operation_id) > 81:
        return JsonResponse({"error": "Invalid operation"}, status=400)
    with transaction.atomic():
        outcome = approve_once(
            reviewer_id=request.user.id,
            case_id=case_id,
            operation_id=operation_id,
        )
        if outcome is None:
            raise Http404()
        if outcome.created:
            transaction.on_commit(wake_approval_outbox)
    return JsonResponse({"status": outcome.status})

Keep CSRF middleware enabled and include the token in the rendered POST form; API clients with another authentication model need an explicit policy rather than a blanket exemption. Reject an empty or malformed operation ID. In a synchronous service, open atomic, select the case for update on a database that supports row locking, verify reviewer permission and transition state, and insert the outcome with a unique operation key. The unique constraint is the final defense against concurrent replays. Register on_commit after the new operation is recorded. The callback should enqueue a durable event or outbox dispatcher, not be the sole proof that a notification exists; an application crash just after commit can still lose an in-process callback.

Cost and boundaries

The transaction holds locks while it validates and writes. Keep network calls outside that lock; waiting for a remote mail service under atomic expands contention and slows unrelated reviewers. A unique index and retained operation records use storage proportional to kept commands, with a retention policy tied to realistic retry windows. A row lock serializes conflicting approvals for one case, which is often the intended behavior, but a hot case can create queueing. CSRF validation adds modest request work and prevents a different class of browser-origin attack than authorization. Measure lock wait, duplicate-command rate, callback lag, and the gap between approval commit and durable notification acceptance.

Failure trace

Submit without a CSRF token and confirm the browser form is rejected before mutation. Send a valid token as reviewer 81 for case 62 and confirm authorization still fails. Race two requests with the same operation ID; only one logical approval and one durable event should remain. Commit successfully, drop the HTTP response, and retry with the same ID to recover the prior result. Force an exception after updating the case but before commit and ensure the state and event both roll back. Crash the process after commit and before a callback runs in a fault test; this exposes why the durable outbox is necessary.

Verification

  • CSRF rejection occurs before a browser mutation.
  • The service owns a unique operation key and current permission check.
  • The durable event commits with the approval; the callback only wakes delivery.

Practice drill

Implement an approval form, command service, operation table, and notification outbox for one permit. Write the database uniqueness and case status constraints before the view. Exercise missing token, malformed ID, forbidden reviewer, stale status, exact replay, and simultaneous replay. Use on_commit to wake the outbox dispatcher only after commit; store the event itself in the same transaction as the approval. Observe the callback timing in tests that commit and in tests that roll back. Explain the remaining retention and delivery decisions in the project's operational notes.

Decision note

Treat CSRF, permission, transaction, replay identity, and durable side effects as separate checks on one command.

Common Mistakes

  • Disabling CSRF because a user is authenticated.
  • Queuing a notification before its database state commits.
  • Treating on_commit alone as durable message storage.

Related lessons

Django Request and Persistence Boundaries; Django Middleware, Sessions, and Object Permissions; Django QuerySet Shape, Prefetch, and Page Cost; Django ASGI, Async Views, and the Sync ORM Boundary; API Mutation and Failure Contracts; Background Workflow Reliability.

Apply and check

Build Project: Django permit review service and review Web Development: Django request and persistence quiz.

web-tech
web-development
Storage details