diff --git a/CHANGES/10049.misc.rst b/CHANGES/10049.misc.rst new file mode 100644 index 00000000000..58f61d48420 --- /dev/null +++ b/CHANGES/10049.misc.rst @@ -0,0 +1 @@ +Improved performance of making requests when there are no auto headers to skip -- by :user:`bdraco`. diff --git a/aiohttp/client_reqrep.py b/aiohttp/client_reqrep.py index 08a4d44b345..73309c61342 100644 --- a/aiohttp/client_reqrep.py +++ b/aiohttp/client_reqrep.py @@ -209,6 +209,8 @@ class ClientRequest: __writer: Optional["asyncio.Task[None]"] = None # async task for streaming data _continue = None # waiter future for '100 Continue' response + _skip_auto_headers: Optional["CIMultiDict[None]"] = None + # N.B. # Adding __del__ method with self._writer closing doesn't make sense # because _writer is instance method, thus it keeps a reference to self. @@ -293,6 +295,10 @@ def __init__( def __reset_writer(self, _: object = None) -> None: self.__writer = None + @property + def skip_auto_headers(self) -> CIMultiDict[None]: + return self._skip_auto_headers or CIMultiDict() + @property def _writer(self) -> Optional["asyncio.Task[None]"]: return self.__writer @@ -404,20 +410,19 @@ def update_headers(self, headers: Optional[LooseHeaders]) -> None: def update_auto_headers(self, skip_auto_headers: Optional[Iterable[str]]) -> None: if skip_auto_headers is not None: - self.skip_auto_headers = CIMultiDict( + self._skip_auto_headers = CIMultiDict( (hdr, None) for hdr in sorted(skip_auto_headers) ) used_headers = self.headers.copy() - used_headers.extend(self.skip_auto_headers) # type: ignore[arg-type] + used_headers.extend(self._skip_auto_headers) # type: ignore[arg-type] else: # Fast path when there are no headers to skip # which is the most common case. - self.skip_auto_headers = CIMultiDict() used_headers = self.headers for hdr, val in self.DEFAULT_HEADERS.items(): if hdr not in used_headers: - self.headers.add(hdr, val) + self.headers[hdr] = val if hdrs.USER_AGENT not in used_headers: self.headers[hdrs.USER_AGENT] = SERVER_SOFTWARE @@ -522,21 +527,20 @@ def update_body_from_data(self, body: Any) -> None: self.body = body # enable chunked encoding if needed - if not self.chunked: - if hdrs.CONTENT_LENGTH not in self.headers: - size = body.size - if size is None: - self.chunked = True - else: - if hdrs.CONTENT_LENGTH not in self.headers: - self.headers[hdrs.CONTENT_LENGTH] = str(size) + if not self.chunked and hdrs.CONTENT_LENGTH not in self.headers: + if (size := body.size) is not None: + self.headers[hdrs.CONTENT_LENGTH] = str(size) + else: + self.chunked = True # copy payload headers assert body.headers + headers = self.headers + skip_headers = self._skip_auto_headers for key, value in body.headers.items(): - if key in self.headers or key in self.skip_auto_headers: + if key in headers or (skip_headers is not None and key in skip_headers): continue - self.headers[key] = value + headers[key] = value def update_expect_continue(self, expect: bool = False) -> None: if expect: @@ -664,7 +668,10 @@ async def send(self, conn: "Connection") -> "ClientResponse": # set default content-type if ( self.method in self.POST_METHODS - and hdrs.CONTENT_TYPE not in self.skip_auto_headers + and ( + self._skip_auto_headers is None + or hdrs.CONTENT_TYPE not in self._skip_auto_headers + ) and hdrs.CONTENT_TYPE not in self.headers ): self.headers[hdrs.CONTENT_TYPE] = "application/octet-stream" diff --git a/tests/test_client_request.py b/tests/test_client_request.py index 176808db6ef..1b5d3a88fff 100644 --- a/tests/test_client_request.py +++ b/tests/test_client_request.py @@ -687,6 +687,7 @@ async def test_content_type_skip_auto_header_bytes( skip_auto_headers={"Content-Type"}, loop=loop, ) + assert req.skip_auto_headers == CIMultiDict({"CONTENT-TYPE": None}) resp = await req.send(conn) assert "CONTENT-TYPE" not in req.headers resp.close()