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.py — Operation.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.
Bug Report
Summary
When Django's
ATOMIC_REQUESTS = Trueis enabled, transactions are not rolled back when an exception is raised inside a Django Ninja endpoint (or a service called by it), even withDEBUG=False. This includes bothHttpErrorand standard Python exceptions likeValueError.Environment
Expected Behavior
Per Django's documentation,
ATOMIC_REQUESTS = Truewraps every request in atransaction.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()inninja/operation.pywraps the entire view call in atry/except Exceptionblock and routes all exceptions toself.api.on_exception().This means no exception ever propagates out of Ninja's view layer to Django's transaction middleware.
For
HttpErrorspecifically, the registered handler inninja/errors.py(_default_http_error) always returns anHttpResponseand never re-raises — regardless of theDEBUGsetting. For generic exceptions,_default_exceptionre-raises only whenDEBUG=False, butHttpErroris matched first via MRO lookup and never reaches that branch.Django's
TransactionMiddlewarereceives a cleanHttpResponsein all cases and interprets it as a successful request → COMMIT.Relevant code path:
ninja/operation.py—Operation.run(), theexcept Exceptionblockninja/errors.py—_default_http_error()(never re-raises)ninja/errors.py—_default_exception()(re-raises only for non-HttpError in DEBUG=False)Minimal Reproduction
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.