Description
traced_middleware_factory in ddtrace/contrib/internal/django/middleware.py drops process_exception (and other custom attributes) when wrapping async function-based Django middleware. This prevents Django from registering exception handlers, causing unhandled exceptions that the middleware is designed to catch.
Root cause
In traced_middleware_factory (line ~130), when the inner middleware function is async:
if iscoroutinefunction(middleware):
async def traced_async_middleware_func(*args, **kwargs):
...
return await middleware(*args, **kwargs)
return traced_async_middleware_func # ← new function, no attributes copied
The sync branch uses wrap(middleware, traced_middleware_func) which preserves attributes via wrapt. The async branch creates a plain async def and returns it, losing all attributes.
Impact
Django's BaseHandler.load_middleware checks hasattr(mw_instance, 'process_exception') to register exception middleware. After ddtrace wraps an async function-based middleware, this check returns False, and _exception_middleware is empty.
Any Django middleware that:
- Is function-based (not class-based)
- Uses
@sync_and_async_middleware
- Attaches
process_exception as a function attribute
...will have its process_exception silently dropped under ASGI when ddtrace is active.
Confirmed affected: allauth's AccountMiddleware — its process_exception handles ReauthenticationRequired (reauthentication redirect flow). Under ASGI with ddtrace, this becomes a 500 instead of a redirect.
Reproduction
import asyncio, sys, types
import django
from django.conf import settings
settings.configure(
DEBUG=False, SECRET_KEY="x", ROOT_URLCONF=__name__,
MIDDLEWARE=[
"django.contrib.sessions.middleware.SessionMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"allauth.account.middleware.AccountMiddleware",
],
INSTALLED_APPS=[
"django.contrib.contenttypes", "django.contrib.auth",
"django.contrib.sessions", "allauth", "allauth.account", "allauth.mfa",
],
DATABASES={"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}},
SESSION_ENGINE="django.contrib.sessions.backends.cache",
CACHES={"default": {"BACKEND": "django.core.cache.backends.locmem.LocMemCache"}},
ACCOUNT_DEFAULT_HTTP_PROTOCOL="http",
)
django.setup()
from django.core.handlers.base import BaseHandler
# Without ddtrace
handler = BaseHandler()
handler.load_middleware(is_async=True)
print(f"Without ddtrace: {len(handler._exception_middleware)} exception middleware")
# Output: 1
# With ddtrace
import ddtrace
ddtrace.patch(django=True)
handler2 = BaseHandler()
handler2.load_middleware(is_async=True)
print(f"With ddtrace: {len(handler2._exception_middleware)} exception middleware")
# Output: 0
Suggested fix
Use contrib_trace_utils.wrap for the async branch (same pattern already used in wrap_middleware_class for async methods), or copy attributes from the original middleware function onto the wrapper:
if iscoroutinefunction(middleware):
async def traced_async_middleware_func(*args, **kwargs):
...
return await middleware(*args, **kwargs)
# Preserve attributes like process_exception
for attr in ("process_exception", "process_view", "process_template_response"):
if hasattr(middleware, attr):
setattr(traced_async_middleware_func, attr, getattr(middleware, attr))
return traced_async_middleware_func
Or use wrapt's FunctionWrapper which handles this transparently.
Environment
- ddtrace: 4.10.2
- Django: 6.0.6
- Python: 3.14.3
- allauth: 65.18.0
Description
traced_middleware_factoryinddtrace/contrib/internal/django/middleware.pydropsprocess_exception(and other custom attributes) when wrapping async function-based Django middleware. This prevents Django from registering exception handlers, causing unhandled exceptions that the middleware is designed to catch.Root cause
In
traced_middleware_factory(line ~130), when the inner middleware function is async:The sync branch uses
wrap(middleware, traced_middleware_func)which preserves attributes via wrapt. The async branch creates a plainasync defand returns it, losing all attributes.Impact
Django's
BaseHandler.load_middlewarecheckshasattr(mw_instance, 'process_exception')to register exception middleware. After ddtrace wraps an async function-based middleware, this check returnsFalse, and_exception_middlewareis empty.Any Django middleware that:
@sync_and_async_middlewareprocess_exceptionas a function attribute...will have its
process_exceptionsilently dropped under ASGI when ddtrace is active.Confirmed affected: allauth's
AccountMiddleware— itsprocess_exceptionhandlesReauthenticationRequired(reauthentication redirect flow). Under ASGI with ddtrace, this becomes a 500 instead of a redirect.Reproduction
Suggested fix
Use
contrib_trace_utils.wrapfor the async branch (same pattern already used inwrap_middleware_classfor async methods), or copy attributes from the original middleware function onto the wrapper:Or use wrapt's FunctionWrapper which handles this transparently.
Environment