[Django] #35414: Issue with AsyncClient ignoring default headers compared to synchronous Client

26 views
Skip to first unread message

Django

unread,
Apr 30, 2024, 12:07:27 AMApr 30
to django-...@googlegroups.com
#35414: Issue with AsyncClient ignoring default headers compared to synchronous
Client
-------------------------------------+-------------------------------------
Reporter: 설원준 | Owner: nobody
(Wonjoon Seol)/Dispatch squad |
Type: Bug | Status: new
Component: HTTP | Version: 5.0
handling | Keywords: AsyncClient,
Severity: Normal | ASGIRequest
Triage Stage: | Has patch: 0
Unreviewed |
Needs documentation: 0 | Needs tests: 0
Patch needs improvement: 0 | Easy pickings: 0
UI/UX: 0 |
-------------------------------------+-------------------------------------
== Description:
Currently, there is an inconsistency between Django's asynchronous
AsyncClient and its synchronous counterpart Client regarding the handling
of default headers. While the synchronous Client correctly includes
default headers, the asynchronous AsyncClient ignores them. This behavior
leads to discrepancies when utilizing fixtures with default headers,
causing tests to fail unexpectedly.


== Reproduction Steps:

Set up a fixture with default headers for both synchronous and
asynchronous clients.
Utilize the fixtures in test cases and observe the behavior.
Notice that the synchronous client includes default headers as expected,
while the asynchronous client does not.


== Code Snippets:
{{{
@pytest.fixture(scope="session")
def jwt_token(token_payload: dict[str, Any]) -> str:
return jwt.encode({"abc", '"abc"}, key='123', algorithm="HS256")

# this passes HTTP_AUTHORIZATION default header
@pytest.fixture(scope="session")
def sync_client_with_token(jwt_token) -> Generator[Client, None, None]:
yield Client(HTTP_AUTHORIZATION=f"Bearer {jwt_token}")

# this does not
@pytest.fixture(scope="session")
async def async_client_with_token(jwt_token) ->
AsyncIterator[AsyncClient]:
async_client = AsyncClient(HTTP_AUTHORIZATION=f"Bearer {jwt_token}")
# async_client.defaults["AUTHORIZATION"] = f"Bearer {jwt_token}"
yield async_client
}}}

AsyncRequestFactory.generic() does not currently check if self.defaults
exists and ASGIRequest only check hard-coded header names in __init__()
method, effectively ignoring rest of the self.scope values.

Note that while RequestFactory.generic() method does not check whether
self.defaults exist but WSGIRequest receives default values via
._base_environ() method when creating WSGIRequest instance.

== Proposed Solutions:

**Fix Method 1: Modify AsyncRequestFactory.generic() method**
{{{
class AsyncRequestFactory(RequestFactory):
def generic(
self,
method,
path,
data="",
content_type="application/octet-stream",
secure=False,
*,
headers=None,
**extra,
):
"""Construct an arbitrary HTTP request."""
parsed = urlparse(str(path)) # path can be lazy.
data = force_bytes(data, settings.DEFAULT_CHARSET)
s = {
"method": method,
"path": self._get_path(parsed),
"server": ("127.0.0.1", "443" if secure else "80"),
"scheme": "https" if secure else "http",
"headers": [(b"host", b"testserver")],
}
if self.defaults: # <- added
extra = {**self.defaults, **extra} <- added
if data:
s["headers"].extend(
[
(b"content-length", str(len(data)).encode("ascii")),
(b"content-type", content_type.encode("ascii")),
]
)
s["_body_file"] = FakePayload(data)
...
}}}


**Fix Method 2: Modify ASGIRequest**
Alternatively, the ASGIRequest class can be adjusted to include default
headers in the META attribute during initialisation.

{{{
class ASGIRequest(HttpRequest):

def __init__(self, scope, body_file):
self.scope = scope
...
if isinstance(query_string, bytes):
query_string = query_string.decode()
self.META = {
**self.scope, <- # Added
"REQUEST_METHOD": self.method,
"QUERY_STRING": query_string,
"SCRIPT_NAME": self.script_name,
"PATH_INFO": self.path_info,
}}}

This would be simliar to WSGIRequest, where self.META = environ is set
during the init phase.
However, it's worth noting that WSGI has a different META format.

**asgi META **
{{{
{'asgi': {'version': '3.0'}, 'type': 'http', 'http_version': '1.1',
'client': ['127.0.0.1', 0], 'server': ('127.0.0.1', '80'), 'scheme':
'http', 'method': 'GET', 'headers': [(b'host', b'testserver'), (b'cookie',
b'')], 'HTTP_AUTHORIZATION': 'Bearer
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3MTQ0NDg2NzcsImV4cCI6MTcxNDQ1MjI3Nywic3ViX2lkIjoiMSIsInBsYXRmb3JtIjoiVk1TLXN0YWdpbmciLCJiYXNlX3VybCI6Imh0dHBzOi8vc3RhZ2luZy1jZW8tcG9ydGFsLWFwaS55b2dpeW8uY28ua3IvIiwicm9sZSI6InN0YWZmIiwidXNlcl9pZCI6IjEiLCJzdGFmZl9ncm91cF9pZCI6bnVsbH0
.WWubd4iOUnsqbWO0ba-8mAsCCk3QDbBONvB_nznZQsk', 'path':
'/campaign/v1/campaigns/1/items/', 'query_string':
'date_type=created_at&date_from=2024-04-28+03%3A44%3A37.484747%2B00%3A00&date_to=2024-05-02+03%3A44%3A37.484759%2B00%3A00',
'REQUEST_METHOD': 'GET', 'QUERY_STRING':
'date_type=created_at&date_from=2024-04-28+03%3A44%3A37.484747%2B00%3A00&date_to=2024-05-02+03%3A44%3A37.484759%2B00%3A00',
'SCRIPT_NAME': '', 'PATH_INFO': '/campaign/v1/campaigns/1/items/',
'wsgi.multithread': True, 'wsgi.multiprocess': True, 'REMOTE_ADDR':
'127.0.0.1', 'REMOTE_HOST': '127.0.0.1', 'REMOTE_PORT': 0, 'SERVER_NAME':
'127.0.0.1', 'SERVER_PORT': '80', 'HTTP_HOST': 'testserver',
'HTTP_COOKIE': ''}
}}}

ASGI META has separate 'headers' but the custom headers are not added
there.

**wsgi META**
{{{
{'HTTP_COOKIE': '', 'PATH_INFO': '/campaign/v1/campaigns/1/items/',
'REMOTE_ADDR': '127.0.0.1', 'REQUEST_METHOD': 'GET', 'SCRIPT_NAME': '',
'SERVER_NAME': 'testserver', 'SERVER_PORT': '80', 'SERVER_PROTOCOL':
'HTTP/1.1', 'wsgi.version': (1, 0), 'wsgi.url_scheme': 'http',
'wsgi.input': <django.test.client.FakePayload object at 0x1061b0d90>,
'wsgi.errors': <_io.BytesIO object at 0x104f42020>, 'wsgi.multiprocess':
True, 'wsgi.multithread': False, 'wsgi.run_once': False,
'HTTP_AUTHORIZATION': 'Bearer
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3MTQ0NDg3OTQsImV4cCI6MTcxNDQ1MjM5NCwic3ViX2lkIjoiMSIsInBsYXRmb3JtIjoiVk1TLXN0YWdpbmciLCJiYXNlX3VybCI6Imh0dHBzOi8vc3RhZ2luZy1jZW8tcG9ydGFsLWFwaS55b2dpeW8uY28ua3IvIiwicm9sZSI6InN0YWZmIiwidXNlcl9pZCI6IjEiLCJzdGFmZl9ncm91cF9pZCI6bnVsbH0.MkbgS1zDaMLdDMLC0_Jpe_2O7VBtJD8km70Y0KlUb4g',
'QUERY_STRING':
'date_type=created_at&date_from=2024-04-28+03%3A46%3A35.172175%2B00%3A00&date_to=2024-05-02+03%3A46%3A35.172190%2B00%3A00'}
}}}


Addressing this inconsistency will ensure that the behaviour of both
synchronous and asynchronous clients remains consistent and predictable
across Django applications.

Thanks.
--
Ticket URL: <https://code.djangoproject.com/ticket/35414>
Django <https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.

Django

unread,
Apr 30, 2024, 12:08:11 AMApr 30
to django-...@googlegroups.com
#35414: Issue with AsyncClient ignoring default headers compared to synchronous
Client
-------------------------------------+-------------------------------------
Reporter: wonjoonSeol-WS | Owner: nobody
Type: Bug | Status: new
Component: HTTP handling | Version: 5.0
Severity: Normal | Resolution:
Keywords: AsyncClient, | Triage Stage:
ASGIRequest | Unreviewed
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Description changed by wonjoonSeol-WS:

Old description:
New description:
--
Ticket URL: <https://code.djangoproject.com/ticket/35414#comment:1>

Django

unread,
Apr 30, 2024, 12:09:56 AMApr 30
to django-...@googlegroups.com
#35414: Issue with AsyncClient ignoring default headers compared to synchronous
Client
-------------------------------------+-------------------------------------
Reporter: wonjoonSeol-WS | Owner: nobody
Type: Bug | Status: new
Component: HTTP handling | Version: 5.0
Severity: Normal | Resolution:
Keywords: AsyncClient, | Triage Stage:
ASGIRequest | Unreviewed
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Description changed by wonjoonSeol-WS:

Old description:

- Set up a fixture with default headers for both synchronous and
asynchronous clients.
- Utilize the fixtures in test cases and observe the behavior.
- Notice that the synchronous client includes default headers as expected,
Ticket URL: <https://code.djangoproject.com/ticket/35414#comment:2>

Django

unread,
Apr 30, 2024, 12:17:52 AMApr 30
to django-...@googlegroups.com
#35414: Issue with AsyncClient ignoring default headers compared to synchronous
Client
-------------------------------------+-------------------------------------
Reporter: 설원준(Wonjoon | Owner: nobody
Seol)/Dispatch squad |
Type: Bug | Status: new
Component: HTTP handling | Version: 5.0
Severity: Normal | Resolution:
Keywords: AsyncClient, | Triage Stage:
ASGIRequest | Unreviewed
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Description changed by 설원준(Wonjoon Seol)/Dispatch squad:

Old description:

> Currently, there is an inconsistency between Django's asynchronous
> AsyncClient and its synchronous counterpart Client regarding the handling
> of default headers. While the synchronous Client correctly includes
> default headers, the asynchronous AsyncClient ignores them. This behavior
> leads to discrepancies when utilizing fixtures with default headers,
> causing tests to fail unexpectedly.
>

> == Reproduction Steps:
>
b'')], 'HTTP_AUTHORIZATION': 'Bearer ...', 'path': '/campaigns/',
'query_string':
'date_type=created_at&date_from=2024-04-28+03%3A44%3A37.484747%2B00%3A00&date_to=2024-05-02+03%3A44%3A37.484759%2B00%3A00',
'REQUEST_METHOD': 'GET', 'QUERY_STRING':
'date_type=created_at&date_from=2024-04-28+03%3A44%3A37.484747%2B00%3A00&date_to=2024-05-02+03%3A44%3A37.484759%2B00%3A00',
'SCRIPT_NAME': '', 'PATH_INFO': '/campaign/v1/campaigns/1/items/',
'wsgi.multithread': True, 'wsgi.multiprocess': True, 'REMOTE_ADDR':
'127.0.0.1', 'REMOTE_HOST': '127.0.0.1', 'REMOTE_PORT': 0, 'SERVER_NAME':
'127.0.0.1', 'SERVER_PORT': '80', 'HTTP_HOST': 'testserver',
'HTTP_COOKIE': ''}
}}}

ASGI META has separate 'headers' but the custom headers are not added
there.

**wsgi META**
{{{
{'HTTP_COOKIE': '', 'PATH_INFO': '/campaigns/', 'REMOTE_ADDR':
'127.0.0.1', 'REQUEST_METHOD': 'GET', 'SCRIPT_NAME': '', 'SERVER_NAME':
'testserver', 'SERVER_PORT': '80', 'SERVER_PROTOCOL': 'HTTP/1.1',
'wsgi.version': (1, 0), 'wsgi.url_scheme': 'http', 'wsgi.input':
<django.test.client.FakePayload object at 0x1061b0d90>, 'wsgi.errors':
<_io.BytesIO object at 0x104f42020>, 'wsgi.multiprocess': True,
'wsgi.multithread': False, 'wsgi.run_once': False, 'HTTP_AUTHORIZATION':
'Bearer ....', 'QUERY_STRING':
'date_type=created_at&date_from=2024-04-28+03%3A46%3A35.172175%2B00%3A00&date_to=2024-05-02+03%3A46%3A35.172190%2B00%3A00'}
}}}


Addressing this inconsistency will ensure that the behaviour of both
synchronous and asynchronous clients remains consistent and predictable
across Django applications.

Thanks.

--
--
Ticket URL: <https://code.djangoproject.com/ticket/35414#comment:3>

Django

unread,
Apr 30, 2024, 12:18:32 AMApr 30
to django-...@googlegroups.com
#35414: Issue with AsyncClient ignoring default headers compared to synchronous
Client
-------------------------------------+-------------------------------------
Reporter: wonjoonSeol-WS | Owner: nobody
Type: Bug | Status: new
Component: HTTP handling | Version: 5.0
Severity: Normal | Resolution:
Keywords: AsyncClient, | Triage Stage:
ASGIRequest | Unreviewed
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Description changed by wonjoonSeol-WS:
'SCRIPT_NAME': '', 'PATH_INFO': '/campaigns/', 'wsgi.multithread': True,
'wsgi.multiprocess': True, 'REMOTE_ADDR': '127.0.0.1', 'REMOTE_HOST':
'127.0.0.1', 'REMOTE_PORT': 0, 'SERVER_NAME': '127.0.0.1', 'SERVER_PORT':
'80', 'HTTP_HOST': 'testserver', 'HTTP_COOKIE': ''}
}}}

ASGI META has separate 'headers' but the custom headers are not added
there.

**wsgi META**
{{{
{'HTTP_COOKIE': '', 'PATH_INFO': '/campaigns/', 'REMOTE_ADDR':
'127.0.0.1', 'REQUEST_METHOD': 'GET', 'SCRIPT_NAME': '', 'SERVER_NAME':
'testserver', 'SERVER_PORT': '80', 'SERVER_PROTOCOL': 'HTTP/1.1',
'wsgi.version': (1, 0), 'wsgi.url_scheme': 'http', 'wsgi.input':
<django.test.client.FakePayload object at 0x1061b0d90>, 'wsgi.errors':
<_io.BytesIO object at 0x104f42020>, 'wsgi.multiprocess': True,
'wsgi.multithread': False, 'wsgi.run_once': False, 'HTTP_AUTHORIZATION':
'Bearer ....', 'QUERY_STRING':
'date_type=created_at&date_from=2024-04-28+03%3A46%3A35.172175%2B00%3A00&date_to=2024-05-02+03%3A46%3A35.172190%2B00%3A00'}
}}}


Addressing this inconsistency will ensure that the behaviour of both
synchronous and asynchronous clients remains consistent and predictable
across Django applications.

Thanks.

--
--
Ticket URL: <https://code.djangoproject.com/ticket/35414#comment:4>

Django

unread,
Apr 30, 2024, 3:31:54 AMApr 30
to django-...@googlegroups.com
#35414: Issue with AsyncClient ignoring default headers compared to synchronous
Client
-------------------------------------+-------------------------------------
Reporter: 설원준(Wonjoon | Owner: nobody
Seol)/Dispatch squad |
Type: Bug | Status: closed
Component: HTTP handling | Version: 5.0
Severity: Normal | Resolution: needsinfo
Keywords: AsyncClient, | Triage Stage:
ASGIRequest | Unreviewed
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Changes (by Sarah Boyce):

* resolution: => needsinfo
* status: new => closed

Comment:

Hi 설원준(Wonjoon Seol)/Dispatch squad 👋
I can see you are using pytest, which is an external dependency, there's a
good chance you are using others.
I need to be able to replicate an issue using **only** Django and
currently it's quite hard to understand what's going on.

I think Trac is not the right place to discuss this at this point, perhaps
you want to ask for support on [https://forum.djangoproject.com/c/users/6
the Django forum] or perhaps you want to raise this with a different
dependency (perhaps [https://github.com/pytest-dev/pytest-django/issues
pytest-django] is more appropriate?).
If you can write a test case for Django that shows the issue without using
pytest, then I think we can reopen the ticket 👍
--
Ticket URL: <https://code.djangoproject.com/ticket/35414#comment:5>

Django

unread,
Apr 30, 2024, 6:17:03 AMApr 30
to django-...@googlegroups.com
#35414: Issue with AsyncClient ignoring default headers compared to synchronous
Client
-------------------------------------+-------------------------------------
Reporter: 설원준(Wonjoon | Owner: nobody
Seol)/Dispatch squad |
Type: Bug | Status: closed
Component: HTTP handling | Version: 5.0
Severity: Normal | Resolution: needsinfo
Keywords: AsyncClient, | Triage Stage:
ASGIRequest | Unreviewed
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Comment (by 설원준(Wonjoon Seol)/Dispatch squad):

Hi Sarah, the pytest fixture is only there to reduce boilerplates.
Here is your requested test case without dependencies.

**polls/views.py**

{{{
from django.http import JsonResponse
def index(request):
data = {"message": "This is an example API response"}
return JsonResponse(data)
}}}


**middleware.py (Not required, can just print self.META inside
ASGIRequest.**
{{{
class JWTMiddleware:
def __init__(self, get_response):
self.get_response = get_response

def __call__(self, request):
if 'HTTP_AUTHORIZATION' not in request.META:
return JsonResponse({'error': 'Authorization header is
missing'}, status=401)
return self.get_response(request)
}}}

**polls/tests.py**
{{{

from http import HTTPStatus

from django.test import TestCase, AsyncClient
from django.urls import reverse


class EXAMPLE_TESTS(TestCase):
async def test_should_return_ok( # Fails
self,
) -> None:
async_client = AsyncClient(HTTP_AUTHORIZATION=f"Bearer
I_AM_JWT_TOKEN") # AUTHORIZATION, HTTP_AUTHORIZATION both fails due to the
reason in the original post.

response = await async_client.get(
reverse("index"),
)

self.assertEqual(response.status_code, HTTPStatus.OK)

async def test_should_return_ok2( # Passes
self,
) -> None:
async_client = AsyncClient()

response = await async_client.get(
reverse("index"),
AUTHORIZATION=f"Bearer I_AM_JWT_TOKEN"
)

self.assertEqual(response.status_code, HTTPStatus.OK)
}}}



{{{
**printing META: (Customer header missing)**
{'REQUEST_METHOD': 'GET', 'QUERY_STRING': '', 'SCRIPT_NAME': '',
'PATH_INFO': '/polls/', 'wsgi.multithread': True, 'wsgi.multiprocess':
True, 'REMOTE_ADDR': '127.0.0.1', 'REMOTE_HOST': '127.0.0.1',
'REMOTE_PORT': 0, 'SERVER_NAME': '127.0.0.1', 'SERVER_PORT': '80',
'HTTP_HOST': 'testserver', 'HTTP_COOKIE': ''}

F
======================================================================
FAIL: test_should_return_ok
(polls.tests.EXAMPLE_TESTS.test_should_return_ok)
----------------------------------------------------------------------
Traceback (most recent call last):
...
File "/Users/.../workspace/django-mvp/mysite/polls/tests.py", line 17,
in test_should_return_ok
self.assertEqual(response.status_code, HTTPStatus.OK)
AssertionError: 401 != <HTTPStatus.OK: 200>

Ran 2 tests in 0.012s

FAILED (failures=1)
}}}
--
Ticket URL: <https://code.djangoproject.com/ticket/35414#comment:6>

Django

unread,
May 3, 2024, 3:59:32 PMMay 3
to django-...@googlegroups.com
#35414: Issue with AsyncClient ignoring default headers compared to synchronous
Client
-------------------------------------+-------------------------------------
Reporter: 설원준(Wonjoon | Owner: nobody
Seol)/Dispatch squad |
Type: Bug | Status: new
Component: HTTP handling | Version: 5.0
Severity: Normal | Resolution:
Keywords: AsyncClient, | Triage Stage:
ASGIRequest | Unreviewed
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Changes (by 설원준(Wonjoon Seol)/Dispatch squad):

* resolution: needsinfo =>
* status: closed => new

--
Ticket URL: <https://code.djangoproject.com/ticket/35414#comment:7>

Django

unread,
May 4, 2024, 6:24:34 AMMay 4
to django-...@googlegroups.com
#35414: Issue with AsyncClient ignoring default headers compared to synchronous
Client
-------------------------------------+-------------------------------------
Reporter: 설원준(Wonjoon | Owner: nobody
Seol)/Dispatch squad |
Type: Bug | Status: closed
Component: HTTP handling | Version: 5.0
Severity: Normal | Resolution: invalid
Keywords: AsyncClient, | Triage Stage:
ASGIRequest | Unreviewed
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Changes (by Mariusz Felisiak):

* resolution: => invalid
* status: new => closed

Comment:

This works as expected and
[https://docs.djangoproject.com/en/5.0/topics/testing/tools/#django.test.AsyncClient
documented]:

''"In the initialization, arbitrary keyword arguments in defaults are
added directly into the ASGI scope."''

If you want to add default headers you should pass them in the `headers`
arguments.
--
Ticket URL: <https://code.djangoproject.com/ticket/35414#comment:8>

Django

unread,
May 4, 2024, 10:20:59 PMMay 4
to django-...@googlegroups.com
#35414: Issue with AsyncClient ignoring default headers compared to synchronous
Client
-------------------------------------+-------------------------------------
Reporter: 설원준(Wonjoon | Owner: nobody
Seol)/Dispatch squad |
Type: Bug | Status: closed
Component: HTTP handling | Version: 5.0
Severity: Normal | Resolution: invalid
Keywords: AsyncClient, | Triage Stage:
ASGIRequest | Unreviewed
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Comment (by 설원준(Wonjoon Seol)/Dispatch squad):

Hi Mariusz,

Thanks for the documentation link. So the async client intended behaviour
is inconsistent with the sync counterpart.

But I should've mentioned in my original post that the headers field does
not work neither.
Because converting the above example test using headers argument still
fails.

{{{
class EXAMPLE_TESTS(TestCase):
async def test_should_return_ok( # FAILS
self,
) -> None:
async_client = AsyncClient(headers={"HTTP_AUTHORIZATION": f"Bearer
I_AM_JWT_TOKEN"})

response = await async_client.get(
reverse("index"),
)

self.assertEqual(response.status_code, HTTPStatus.OK)

async def test_should_return_ok2( # Passes
self,
) -> None:
async_client = AsyncClient()

response = await async_client.get(
reverse("index"),
AUTHORIZATION=f"Bearer I_AM_JWT_TOKEN"
)

self.assertEqual(response.status_code, HTTPStatus.OK)
}}}


The reason is still due the the original post.


{{{
def _base_scope(self, **request):
"""The base scope for a request."""
# This is a minimal valid ASGI scope, plus:
# - headers['cookie'] for cookie support,
# - 'client' often useful, see #8551.
scope = {
"asgi": {"version": "3.0"},
"type": "http",
"http_version": "1.1",
"client": ["127.0.0.1", 0],
"server": ("testserver", "80"),
"scheme": "http",
"method": "GET",
"headers": [], # <- scope ignores default header
**self.defaults,
**request,
}
scope["headers"].append(
(
b"cookie",
b"; ".join(
sorted(
("%s=%s" % (morsel.key,
morsel.coded_value)).encode("ascii")
for morsel in self.cookies.values()
)
),
)
)
return scope
}}}

the scope only takes in default argument but ignores default header.

Using the example test, this is the constructed META header after the
initialization.
The default headers are still missing:

{{{
{'REQUEST_METHOD': 'GET', 'QUERY_STRING': '', 'SCRIPT_NAME': '',
'PATH_INFO': '/polls/', 'wsgi.multithread': True, 'wsgi.multiprocess':
True, 'REMOTE_ADDR': '127.0.0.1', 'REMOTE_HOST': '127.0.0.1',
'REMOTE_PORT': 0, 'SERVER_NAME': '127.0.0.1', 'SERVER_PORT': '80',
'HTTP_HOST': 'testserver', 'HTTP_COOKIE': ''}
}}}

Thanks.
--
Ticket URL: <https://code.djangoproject.com/ticket/35414#comment:9>

Django

unread,
May 7, 2024, 6:47:57 AMMay 7
to django-...@googlegroups.com
#35414: Issue with AsyncClient ignoring default headers compared to synchronous
Client
-------------------------------------+-------------------------------------
Reporter: 설원준(Wonjoon | Owner: nobody
Seol)/Dispatch squad |
Type: Bug | Status: new
Component: HTTP handling | Version: 5.0
Severity: Normal | Resolution:
Keywords: AsyncClient, | Triage Stage:
ASGIRequest | Unreviewed
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Changes (by 설원준(Wonjoon Seol)/Dispatch squad):

* resolution: invalid =>
* status: closed => new

--
Ticket URL: <https://code.djangoproject.com/ticket/35414#comment:10>

Django

unread,
May 9, 2024, 5:15:16 PMMay 9
to django-...@googlegroups.com
#35414: Issue with AsyncClient ignoring default headers compared to synchronous
Client
-------------------------------------+-------------------------------------
Reporter: 설원준(Wonjoon | Owner: nobody
Seol)/Dispatch squad |
Type: Bug | Status: new
Component: HTTP handling | Version: 5.0
Severity: Normal | Resolution:
Keywords: AsyncClient, | Triage Stage:
ASGIRequest | Unreviewed
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Changes (by Natalia Bidart):

* cc: Andrew Godwin, Carlton Gibson (added)

Comment:

I've been thinking about this report for a while, and after making some
experiments, I think this is a valid issue. Originally I thought this was
a dupe of #32159 but I don't think that's the case.

What I can't put my finger on is whether there is an issue with the docs,
or if the headers should really be added to the ASGI scope `headers` key
(as the reporter says). The docs shows:

> AsyncClient has the same methods and signatures as the synchronous
(normal) test client, with the following exceptions:
> In the initialization, arbitrary keyword arguments in defaults are added
directly into the ASGI scope.
> Headers passed as extra keyword arguments should not have the HTTP_
prefix required by the synchronous client (see Client.get()). For example,
here is how to set an HTTP Accept header:

To me this implies that the `headers` parameter at class instantiation
time could be use to set parameters to be used in every client operation
(just like with `Client`). But when using the test case provided by the
reporter this is not the case (using a simple Django app with the provided
middleware and a simple view):

{{{#!python
from django.test import TestCase, AsyncClient


class Ticket35414Tests(TestCase):

async def test_should_return_ok(self):
async_client = AsyncClient(headers={"AUTHORIZATION": "A Token"})
response = await async_client.get("/ticket_35414/")
self.assertEqual(response.status_code, 200)
}}}

Failure:
{{{
======================================================================
FAIL: test_should_return_ok
(ticket_35414.tests.Ticket35414Tests.test_should_return_ok)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/home/nessita/.virtualenvs/djangodev/lib/python3.11/site-
packages/asgiref/sync.py", line 254, in __call__
return call_result.result()
^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.11/concurrent/futures/_base.py", line 449, in
result
return self.__get_result()
^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.11/concurrent/futures/_base.py", line 401, in
__get_result
raise self._exception
File "/home/nessita/.virtualenvs/djangodev/lib/python3.11/site-
packages/asgiref/sync.py", line 331, in main_wrap
result = await self.awaitable(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/nessita/fellowship/projectfromrepo/ticket_35414/tests.py",
line 9, in test_should_return_ok
self.assertEqual(response.status_code, 200)
AssertionError: 401 != 200
}}}

Carlton, Andrew, would you have an opinion?
--
Ticket URL: <https://code.djangoproject.com/ticket/35414#comment:11>

Django

unread,
May 9, 2024, 5:16:49 PMMay 9
to django-...@googlegroups.com
#35414: Issue with AsyncClient ignoring default headers compared to synchronous
Client
-------------------------------------+-------------------------------------
Reporter: 설원준(Wonjoon | Owner: nobody
Seol)/Dispatch squad |
Type: Bug | Status: new
Component: HTTP handling | Version: 5.0
Severity: Normal | Resolution:
Keywords: AsyncClient, | Triage Stage: Accepted
ASGIRequest |
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Changes (by Natalia Bidart):

* stage: Unreviewed => Accepted

--
Ticket URL: <https://code.djangoproject.com/ticket/35414#comment:12>

Django

unread,
May 10, 2024, 1:41:01 AMMay 10
to django-...@googlegroups.com
#35414: Issue with AsyncClient ignoring default headers compared to synchronous
Client
-------------------------------------+-------------------------------------
Reporter: 설원준(Wonjoon | Owner: nobody
Seol)/Dispatch squad |
Type: Bug | Status: new
Component: HTTP handling | Version: 5.0
Severity: Normal | Resolution:
Keywords: AsyncClient, | Triage Stage: Accepted
ASGIRequest |
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Comment (by Carlton Gibson):

This looks correct to me.

The `generic()` method takes in `headers` and `extra` and combines them
before passing on to `_base_scope()`:


{{{
def generic(...):
...
if headers:
extra.update(HttpHeaders.to_asgi_names(headers))
s["headers"] += [
(key.lower().encode("ascii"), value.encode("latin1"))
for key, value in extra.items()
]
return self.request(**s)
}}}

But then `_base_scope()` isn't then correctly setting the `headers` key in
the scope (as reported):


{{{
def _base_scope(..., **request):
...
"headers": [], # <- scope ignores default header
**self.defaults,
**request,
}}}
--
Ticket URL: <https://code.djangoproject.com/ticket/35414#comment:13>
Reply all
Reply to author
Forward
0 new messages