Skip to content

[BUG] ATOMIC_REQUESTS = True does not roll back transactions when exceptions are raised inside view functions #1704

@javialon26

Description

@javialon26

Bug Report

Summary

When Django's ATOMIC_REQUESTS = True is enabled, transactions are not rolled back when an exception is raised inside a Django Ninja endpoint (or a service called by it), even with DEBUG=False. This includes both HttpError and standard Python exceptions like ValueError.

Environment

  • django-ninja: 1.6.2
  • Django: 5.2
  • ATOMIC_REQUESTS: True

Expected Behavior

Per Django's documentation, ATOMIC_REQUESTS = True wraps every request in a transaction.atomic() block. Any exception that propagates out of the view should trigger a rollback of all database writes made during that request.

Actual Behavior

Database writes made before the exception are committed, not rolled back.

Root Cause

Operation.run() in ninja/operation.py wraps the entire view call in a try/except Exception block and routes all exceptions to self.api.on_exception().

This means no exception ever propagates out of Ninja's view layer to Django's transaction middleware.

For HttpError specifically, the registered handler in ninja/errors.py (_default_http_error) always returns an HttpResponse and never re-raises — regardless of the DEBUG setting. For generic exceptions, _default_exception re-raises only when DEBUG=False, but HttpError is matched first via MRO lookup and never reaches that branch.

Django's TransactionMiddleware receives a clean HttpResponse in all cases and interprets it as a successful request → COMMIT.
Relevant code path:

  • ninja/operation.pyOperation.run(), the except Exception block
  • ninja/errors.py_default_http_error() (never re-raises)
  • ninja/errors.py_default_exception() (re-raises only for non-HttpError in DEBUG=False)

Minimal Reproduction

# models.py
class MyRecord(models.Model):
    name = models.CharField(max_length=100)
# api.py
@router.post("/test/")
def test_endpoint(request):
    MyRecord.objects.create(name="should be rolled back")
    raise HttpError(409, "conflict")  # record is NOT rolled back

With ATOMIC_REQUESTS = True, MyRecord is persisted in the database despite the HttpError(409) being raised.

Suggested Fix

Operation.run() should re-raise exceptions after calling on_exception() returns, OR Django Ninja could mark the transaction for rollback explicitly (via transaction.set_rollback(True)) before returning the error response.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions