Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
84 commits
Select commit Hold shift + click to select a range
18564e9
Refactor ClientRequest
Dreamsorcerer May 25, 2025
f4d733f
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 25, 2025
22d724a
Add kwargs
Dreamsorcerer May 25, 2025
6fa8996
Merge branch 'clientrequest-refactor' of github.com:aio-libs/aiohttp …
Dreamsorcerer May 25, 2025
8257048
Tweaks
Dreamsorcerer May 25, 2025
7171e0a
Fix
Dreamsorcerer May 25, 2025
9023653
Fix
Dreamsorcerer May 25, 2025
6a1e81a
Fix benchmarks
Dreamsorcerer May 25, 2025
ef47363
Fix benchmarks
Dreamsorcerer May 25, 2025
477d52d
Remove unneeded code
Dreamsorcerer May 25, 2025
8fbac96
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 25, 2025
cb5f104
Fix
Dreamsorcerer May 25, 2025
3820c39
Merge branch 'clientrequest-refactor' of github.com:aio-libs/aiohttp …
Dreamsorcerer May 25, 2025
2559a0b
Merge branch 'master' into clientrequest-refactor
Dreamsorcerer May 25, 2025
19dfe3a
Readd shortcut
Dreamsorcerer May 25, 2025
580387b
Merge branch 'clientrequest-refactor' of github.com:aio-libs/aiohttp …
Dreamsorcerer May 25, 2025
0cdf184
Tweaks
Dreamsorcerer May 25, 2025
ecc6dac
Merge branch 'master' into clientrequest-refactor
Dreamsorcerer Aug 24, 2025
c338a6d
Reduce diff
Dreamsorcerer Aug 24, 2025
b12ebae
Update client_reqrep.py
Dreamsorcerer Aug 25, 2025
b95493c
Update client_reqrep.py
Dreamsorcerer Aug 25, 2025
e4cc1dc
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 25, 2025
be3299c
Fixes
Dreamsorcerer Aug 26, 2025
ca57cab
Fixes
Dreamsorcerer Aug 27, 2025
4b93f0f
Create fixture
Dreamsorcerer Aug 28, 2025
7666c36
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 28, 2025
0332916
Fixes
Dreamsorcerer Aug 28, 2025
500a4fb
Fixes
Dreamsorcerer Aug 28, 2025
f4ebcb7
Fixes
Dreamsorcerer Aug 28, 2025
15a0fa8
Fixes
Dreamsorcerer Aug 28, 2025
deec35d
Fixes
Dreamsorcerer Aug 28, 2025
8f462a2
Fixes
Dreamsorcerer Aug 28, 2025
c20f2ce
Fixes
Dreamsorcerer Aug 28, 2025
28f1369
Fixes
Dreamsorcerer Aug 28, 2025
77137bf
Update conftest.py
Dreamsorcerer Aug 31, 2025
23029d2
Update conftest.py
Dreamsorcerer Aug 31, 2025
0a5651b
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 31, 2025
03e5abf
Update client_reqrep.py
Dreamsorcerer Aug 31, 2025
39aa3f2
Update test_client_request.py
Dreamsorcerer Aug 31, 2025
e4f0eb4
Update test_client_request.py
Dreamsorcerer Aug 31, 2025
da1e507
Update client_reqrep.py
Dreamsorcerer Aug 31, 2025
2989a2c
Update test_client_request.py
Dreamsorcerer Aug 31, 2025
8c95f70
Update test_client_request.py
Dreamsorcerer Aug 31, 2025
6f5011e
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 31, 2025
ddb67a5
Merge branch 'master' into clientrequest-refactor
Dreamsorcerer Oct 14, 2025
a823273
Update client_reqrep.py
Dreamsorcerer Oct 14, 2025
ddc69ad
Update client_reqrep.py
Dreamsorcerer Oct 14, 2025
13db9c9
Update client_reqrep.py
Dreamsorcerer Oct 14, 2025
dd207ec
Fixes
Dreamsorcerer Oct 14, 2025
d1aadac
Fixes
Dreamsorcerer Oct 14, 2025
399ad18
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 14, 2025
52e3776
Fixes
Dreamsorcerer Oct 14, 2025
7c7af5d
Merge branch 'clientrequest-refactor' of github.com:aio-libs/aiohttp …
Dreamsorcerer Oct 14, 2025
583eca6
Fixes
Dreamsorcerer Oct 14, 2025
1c82693
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 14, 2025
54e9ee7
Update test_proxy.py
Dreamsorcerer Oct 15, 2025
89f6de2
Merge branch 'master' into clientrequest-refactor
Dreamsorcerer Oct 15, 2025
4c8df69
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 15, 2025
57e84cc
Fixes
Dreamsorcerer Oct 15, 2025
861fa1a
Fixes
Dreamsorcerer Oct 15, 2025
4895d27
Fixes
Dreamsorcerer Oct 15, 2025
3bb91ca
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 15, 2025
e929ff5
Fixes
Dreamsorcerer Oct 15, 2025
6145b46
Merge branch 'clientrequest-refactor' of github.com:aio-libs/aiohttp …
Dreamsorcerer Oct 15, 2025
157be4d
Fixes
Dreamsorcerer Oct 15, 2025
1d542e6
Fixes
Dreamsorcerer Oct 15, 2025
6855d0a
Fixes
Dreamsorcerer Oct 15, 2025
61f9aa7
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 15, 2025
cb9ec6e
Fixes
Dreamsorcerer Oct 15, 2025
f1549ea
Fixes
Dreamsorcerer Oct 15, 2025
c392b49
Create 11012.breaking.rst
Dreamsorcerer Oct 15, 2025
f19ab2a
solve one perf regression
bdraco Oct 18, 2025
9eceeef
build request info once
bdraco Oct 18, 2025
238096f
lazy construct RequestInfo
bdraco Oct 18, 2025
5780c0a
Update CHANGES/11012.breaking.rst
Dreamsorcerer Oct 19, 2025
fec5fef
str annotations
Dreamsorcerer Oct 19, 2025
9c0fa1c
Update aiohttp/client_reqrep.py
Dreamsorcerer Oct 24, 2025
101fab3
Remove request_info parameter
Dreamsorcerer Oct 24, 2025
8a33e42
Update tests
Dreamsorcerer Oct 24, 2025
4ed803f
Update tests
Dreamsorcerer Oct 24, 2025
b18d623
Unused import
Dreamsorcerer Oct 24, 2025
cde1e76
Fix
Dreamsorcerer Oct 24, 2025
34c37f2
Fix
Dreamsorcerer Oct 24, 2025
740a95f
Fix
Dreamsorcerer Oct 24, 2025
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
Prev Previous commit
Next Next commit
Fixes
  • Loading branch information
Dreamsorcerer committed Aug 26, 2025
commit be3299cb35f1b41f7f035cbc586085b01ac09514
80 changes: 80 additions & 0 deletions aiohttp/client_reqrep.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
from multidict import CIMultiDict, CIMultiDictProxy, MultiDict, MultiDictProxy
from yarl import URL

from . import hdrs, multipart, payload

Check notice

Code scanning / CodeQL

Cyclic import Note

Import of module
aiohttp.multipart
begins an import cycle.

Copilot Autofix

AI 2 months ago

The best way to fix the cyclic import is to move any direct reference to multipart from module-level imports to within the function(s) or method(s) that actually require it. If multipart functionality is only used in a few places, these can import multipart locally within the required function. This minimizes the scope of the cyclic import and breaks the cycle at module load time.

Specifically, in aiohttp/client_reqrep.py, line 19 (from . import hdrs, multipart, payload) should be split so that multipart is imported only where necessary, i.e., move import multipart inside the function(s) or class(es) that use it. Leave hdrs and payload in the module-level import if they do not create cycles.

To implement:

  1. Remove multipart from the module-level grouped import (line 19).
  2. For each instance of multipart usage in this file, add a local import: from . import multipart inside the function/method just before it is used.
  3. Leave all other logic intact.
  4. Do not otherwise modify functionality; only move the import for cycle-breaking.
Suggested changeset 1
aiohttp/client_reqrep.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/aiohttp/client_reqrep.py b/aiohttp/client_reqrep.py
--- a/aiohttp/client_reqrep.py
+++ b/aiohttp/client_reqrep.py
@@ -16,7 +16,7 @@
 from multidict import CIMultiDict, CIMultiDictProxy, MultiDict, MultiDictProxy
 from yarl import URL
 
-from . import hdrs, multipart, payload
+from . import hdrs, payload
 from ._cookie_helpers import (
     parse_cookie_header,
     parse_set_cookie_headers,
EOF
@@ -16,7 +16,7 @@
from multidict import CIMultiDict, CIMultiDictProxy, MultiDict, MultiDictProxy
from yarl import URL

from . import hdrs, multipart, payload
from . import hdrs, payload
from ._cookie_helpers import (
parse_cookie_header,
parse_set_cookie_headers,
Copilot is powered by AI and may make mistakes. Always verify output.
Unable to commit as this autofix suggestion is now outdated
from ._cookie_helpers import (
parse_cookie_header,
parse_set_cookie_headers,
Expand Down Expand Up @@ -1192,6 +1192,86 @@
continue
headers[key] = value

def _update_body(self, body: Any) -> None:
"""Update request body after its already been set."""
# Remove existing Content-Length header since body is changing
if hdrs.CONTENT_LENGTH in self.headers:
del self.headers[hdrs.CONTENT_LENGTH]

# Remove existing Transfer-Encoding header to avoid conflicts
if self.chunked and hdrs.TRANSFER_ENCODING in self.headers:
del self.headers[hdrs.TRANSFER_ENCODING]

# Now update the body using the existing method
# Called from _update_body, add 1 to stacklevel from caller
self.update_body_from_data(body, _stacklevel=4)

# Update transfer encoding headers if needed (same logic as __init__)
if body is not None or self.method not in self.GET_METHODS:
self.update_transfer_encoding()

async def update_body(self, body: Any) -> None:
"""
Update request body and close previous payload if needed.

This method safely updates the request body by first closing any existing
payload to prevent resource leaks, then setting the new body.

IMPORTANT: Always use this method instead of setting request.body directly.
Direct assignment to request.body will leak resources if the previous body
contains file handles, streams, or other resources that need cleanup.

Args:
body: The new body content. Can be:
- bytes/bytearray: Raw binary data
- str: Text data (will be encoded using charset from Content-Type)
- FormData: Form data that will be encoded as multipart/form-data
- Payload: A pre-configured payload object
- AsyncIterable: An async iterable of bytes chunks
- File-like object: Will be read and sent as binary data
- None: Clears the body

Usage:
# CORRECT: Use update_body
await request.update_body(b"new request data")

# WRONG: Don't set body directly
# request.body = b"new request data" # This will leak resources!

# Update with form data
form_data = FormData()
form_data.add_field('field', 'value')
await request.update_body(form_data)

# Clear body
await request.update_body(None)

Note:
This method is async because it may need to close file handles or
other resources associated with the previous payload. Always await
this method to ensure proper cleanup.

Warning:
Setting request.body directly is highly discouraged and can lead to:
- Resource leaks (unclosed file handles, streams)
- Memory leaks (unreleased buffers)
- Unexpected behavior with streaming payloads

It is not recommended to change the payload type in middleware. If the
body was already set (e.g., as bytes), it's best to keep the same type
rather than converting it (e.g., to str) as this may result in unexpected
behavior.

See Also:
- update_body_from_data: Synchronous body update without cleanup
- body property: Direct body access (STRONGLY DISCOURAGED)

"""
# Close existing payload if it exists and needs closing
if self._body is not None:
await self._body.close()
self._update_body(body)

def _update_expect_continue(self, expect: bool = False) -> None:
if expect:
self.headers[hdrs.EXPECT] = "100-continue"
Expand Down
6 changes: 3 additions & 3 deletions tests/test_client_functional.py
Original file line number Diff line number Diff line change
Expand Up @@ -1715,7 +1715,7 @@ async def handler(request: web.Request) -> web.Response:

write_mock = None
writelines_mock = None
original_write_bytes = ClientRequest.write_bytes
original_write_bytes = ClientRequest._write_bytes

async def write_bytes(
self: ClientRequest,
Expand Down Expand Up @@ -1745,7 +1745,7 @@ async def write_bytes(
):
await original_write_bytes(self, writer, conn, content_length)

with mock.patch.object(ClientRequest, "write_bytes", write_bytes):
with mock.patch.object(ClientRequest, "_write_bytes", write_bytes):
app = web.Application()
app.router.add_get("/", handler)
client = await aiohttp_client(app)
Expand Down Expand Up @@ -1775,7 +1775,7 @@ async def test_GET_DEFLATE_no_body(aiohttp_client: AiohttpClient) -> None:
async def handler(request: web.Request) -> web.Response:
return web.json_response({"ok": True})

with mock.patch.object(ClientRequest, "write_bytes") as mock_write_bytes:
with mock.patch.object(ClientRequest, "_write_bytes") as mock_write_bytes:
app = web.Application()
app.router.add_get("/", handler)
client = await aiohttp_client(app)
Expand Down
5 changes: 1 addition & 4 deletions tests/test_client_middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -1083,10 +1083,7 @@ def __init__(self, secretkey: str) -> None:
self.secretkey = secretkey

def get_hash(self, request: ClientRequest) -> str:
if request.body:
data = request.body.decode("utf-8")
else:
data = "{}"
data = request.body.decode("utf-8") or "{}"

# Simulate authentication hash without using real crypto
return f"SIGNATURE-{self.secretkey}-{len(data)}-{data[:10]}"
Expand Down
60 changes: 30 additions & 30 deletions tests/test_client_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ def maker(method: str, url: str, **kwargs: Any) -> ClientRequest:

yield maker
if request is not None:
loop.run_until_complete(request.close())
loop.run_until_complete(request._close())


@pytest.fixture
Expand Down Expand Up @@ -146,13 +146,13 @@ def test_request_info(make_request: _RequestMaker) -> None:
req = make_request("get", "http://python.org/")
url = URL("http://python.org/")
h = CIMultiDictProxy(req.headers)
assert req.request_info == aiohttp.RequestInfo(url, "GET", h, url)
assert req._request_info == aiohttp.RequestInfo(url, "GET", h, url)


def test_request_info_with_fragment(make_request: _RequestMaker) -> None:
req = make_request("get", "http://python.org/#urlfragment")
h = CIMultiDictProxy(req.headers)
assert req.request_info == aiohttp.RequestInfo(
assert req._request_info == aiohttp.RequestInfo(
URL("http://python.org/"),
"GET",
h,
Expand All @@ -167,57 +167,57 @@ def test_version_err(make_request: _RequestMaker) -> None:

def test_host_port_default_http(make_request: _RequestMaker) -> None:
req = make_request("get", "http://python.org/")
assert req.host == "python.org"
assert req.port == 80
assert req.url.host == "python.org"
assert req.url.port == 80
assert not req.is_ssl()


def test_host_port_default_https(make_request: _RequestMaker) -> None:
req = make_request("get", "https://python.org/")
assert req.host == "python.org"
assert req.port == 443
assert req.url.host == "python.org"
assert req.url.port == 443
assert req.is_ssl()


def test_host_port_nondefault_http(make_request: _RequestMaker) -> None:
req = make_request("get", "http://python.org:960/")
assert req.host == "python.org"
assert req.port == 960
assert req.url.host == "python.org"
assert req.url.port == 960
assert not req.is_ssl()


def test_host_port_nondefault_https(make_request: _RequestMaker) -> None:
req = make_request("get", "https://python.org:960/")
assert req.host == "python.org"
assert req.port == 960
assert req.url.host == "python.org"
assert req.url.port == 960
assert req.is_ssl()


def test_host_port_default_ws(make_request: _RequestMaker) -> None:
req = make_request("get", "ws://python.org/")
assert req.host == "python.org"
assert req.port == 80
assert req.url.host == "python.org"
assert req.url.port == 80
assert not req.is_ssl()


def test_host_port_default_wss(make_request: _RequestMaker) -> None:
req = make_request("get", "wss://python.org/")
assert req.host == "python.org"
assert req.port == 443
assert req.url.host == "python.org"
assert req.url.port == 443
assert req.is_ssl()


def test_host_port_nondefault_ws(make_request: _RequestMaker) -> None:
req = make_request("get", "ws://python.org:960/")
assert req.host == "python.org"
assert req.port == 960
assert req.url.host == "python.org"
assert req.url.port == 960
assert not req.is_ssl()


def test_host_port_nondefault_wss(make_request: _RequestMaker) -> None:
req = make_request("get", "wss://python.org:960/")
assert req.host == "python.org"
assert req.port == 960
assert req.url.host == "python.org"
assert req.url.port == 960
assert req.is_ssl()


Expand Down Expand Up @@ -394,29 +394,29 @@ def test_no_path(make_request: _RequestMaker) -> None:

def test_ipv6_default_http_port(make_request: _RequestMaker) -> None:
req = make_request("get", "http://[2001:db8::1]/")
assert req.host == "2001:db8::1"
assert req.port == 80
assert req.url.host == "2001:db8::1"
assert req.url.port == 80
assert not req.is_ssl()


def test_ipv6_default_https_port(make_request: _RequestMaker) -> None:
req = make_request("get", "https://[2001:db8::1]/")
assert req.host == "2001:db8::1"
assert req.port == 443
assert req.url.host == "2001:db8::1"
assert req.url.port == 443
assert req.is_ssl()


def test_ipv6_nondefault_http_port(make_request: _RequestMaker) -> None:
req = make_request("get", "http://[2001:db8::1]:960/")
assert req.host == "2001:db8::1"
assert req.port == 960
assert req.url.host == "2001:db8::1"
assert req.url.port == 960
assert not req.is_ssl()


def test_ipv6_nondefault_https_port(make_request: _RequestMaker) -> None:
req = make_request("get", "https://[2001:db8::1]:960/")
assert req.host == "2001:db8::1"
assert req.port == 960
assert req.url.host == "2001:db8::1"
assert req.url.port == 960
assert req.is_ssl()


Expand Down Expand Up @@ -445,14 +445,14 @@ def test_basic_auth_from_url(make_request: _RequestMaker) -> None:
req = make_request("get", "http://nkim:[email protected]")
assert "AUTHORIZATION" in req.headers
assert "Basic bmtpbToxMjM0" == req.headers["AUTHORIZATION"]
assert "python.org" == req.host
assert "python.org" == req.url.host


def test_basic_auth_no_user_from_url(make_request: _RequestMaker) -> None:
req = make_request("get", "http://:[email protected]")
assert "AUTHORIZATION" in req.headers
assert "Basic OjEyMzQ=" == req.headers["AUTHORIZATION"]
assert "python.org" == req.host
assert "python.org" == req.url.host


def test_basic_auth_from_url_overridden(make_request: _RequestMaker) -> None:
Expand All @@ -461,7 +461,7 @@ def test_basic_auth_from_url_overridden(make_request: _RequestMaker) -> None:
)
assert "AUTHORIZATION" in req.headers
assert "Basic bmtpbToxMjM0" == req.headers["AUTHORIZATION"]
assert "python.org" == req.host
assert "python.org" == req.url.host


def test_path_is_not_double_encoded1(make_request: _RequestMaker) -> None:
Expand Down
2 changes: 1 addition & 1 deletion tests/test_client_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -1052,7 +1052,7 @@ def test_default_encoding_is_utf8() -> None:
timer=TimerNoop(),
traces=[],
loop=mock.Mock(),
session=None, # type: ignore[arg-type]
session=None,
)
response._headers = CIMultiDictProxy(CIMultiDict({}))
response._body = b""
Expand Down
2 changes: 1 addition & 1 deletion tests/test_client_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -949,7 +949,7 @@ async def on_request_headers_sent(
context: object,
params: tracing.TraceRequestHeadersSentParams,
) -> None:
gathered_req_headers.extend(**params.headers)
gathered_req_headers.extend(params.headers)

trace_config = aiohttp.TraceConfig(
trace_config_ctx_factory=mock.Mock(return_value=trace_config_ctx)
Expand Down
Loading