Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 32 additions & 6 deletions ddtrace/contrib/internal/django/middleware.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from inspect import iscoroutinefunction
from inspect import isfunction
from types import FunctionType
from typing import Any
Expand Down Expand Up @@ -134,17 +135,42 @@ def traced_auth_middleware_process_request(func: FunctionType, args: Tuple[Any],
def traced_middleware_factory(func: FunctionType, args: Tuple[Any], kwargs: Dict[str, Any]) -> Any:
middleware = func(*args, **kwargs)

if isfunction(middleware):
if hasattr(func, "__module__") and hasattr(func, "__qualname__"):
resource = f"{func.__module__}.{func.__qualname__}"
else:
resource = func_name(func)
if not isfunction(middleware):
return middleware

if hasattr(func, "__module__") and hasattr(func, "__qualname__"):
resource = f"{func.__module__}.{func.__qualname__}"
else:
resource = func_name(func)

if iscoroutinefunction(middleware):
# Handle async middleware - create async wrapper
async def traced_async_middleware_func(*args, **kwargs):
# The first argument for all middleware is the request object
# DEV: Do `optional=true` to avoid raising an error for middleware that don't follow the convention
# DEV: This is a function, so no `self` argument, so request is at position 0
request = get_argument_value(args, kwargs, 0, "request", optional=True)

with core.context_with_data(
"django.middleware.func",
span_name="django.middleware",
resource=resource,
tags={
COMPONENT: config_django.integration_name,
},
tracer=config_django._tracer,
request=request,
):
return await middleware(*args, **kwargs)

return traced_async_middleware_func
else:
# Handle sync middleware - use original wrapping approach
def traced_middleware_func(func: FunctionType, args: Tuple[Any], kwargs: Dict[str, Any]) -> Any:
# The first argument for all middleware is the request object
# DEV: Do `optional=true` to avoid raising an error for middleware that don't follow the convention
# DEV: This is a function, so no `self` argument, so request is at position 0
request = get_argument_value(args, kwargs, 0, "request")
request = get_argument_value(args, kwargs, 0, "request", optional=True)

with core.context_with_data(
"django.middleware.func",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
fixes:
- |
django: Fixes issue causing ``ValueError: coroutine already executing`` on Python 3.13+ with ``django.utils.decorators.async_only_middleware``.
4 changes: 4 additions & 0 deletions tests/contrib/django/django_app/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@
"tests.contrib.django.middleware.EverythingMiddleware",
]

if os.getenv("TEST_INCLUDE_ASYNC_ONLY_MIDDLEWARE") == "1":
# DEV: Add to the front, since adding at the end causes it to not get called?
MIDDLEWARE = ["tests.contrib.django.middleware.async_only_middleware"] + MIDDLEWARE

INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
Expand Down
15 changes: 15 additions & 0 deletions tests/contrib/django/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,18 @@ def process_view(self, request, view_func, view_args, view_kwargs):

def process_template_response(self, req, resp):
return resp


try:
from django.utils.decorators import async_only_middleware

@async_only_middleware
def my_async_only_middleware(get_response):
async def handle(request):
raise Exception()
return await get_response(request)

return handle

except ImportError:
pass
3 changes: 2 additions & 1 deletion tests/contrib/django/test_django_snapshots.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,8 @@ def test_psycopg3_query_default(always_create_database_spans: bool, client, snap
)
@pytest.mark.parametrize("django_asgi", ["application", "channels_application"])
def test_asgi_200(django_asgi):
with daphne_client(django_asgi) as (client, _):
env = {"TEST_INCLUDE_ASYNC_ONLY_MIDDLEWARE": "1"}
with daphne_client(django_asgi, additional_env=env) as (client, _):
resp = client.get("/", timeout=10)
assert resp.status_code == 200
assert resp.content == b"Hello, test app."
Expand Down
Loading
Loading