@@ -205,7 +205,7 @@ def test_host_port_nondefault_wss(make_request) -> None:
205205
206206def test_host_port_none_port (make_request ) -> None :
207207 req = make_request ("get" , "unix://localhost/path" )
208- assert req .headers ["Host" ] == "localhost"
208+ assert req .headers [hdrs . HOST ] == "localhost"
209209
210210
211211def test_host_port_err (make_request ) -> None :
@@ -220,17 +220,17 @@ def test_hostname_err(make_request) -> None:
220220
221221def test_host_header_host_first (make_request ) -> None :
222222 req = make_request ("get" , "http://python.org/" )
223- assert list (req .headers )[0 ] == "Host"
223+ assert list (req .headers )[0 ] == hdrs . HOST
224224
225225
226226def test_host_header_host_without_port (make_request ) -> None :
227227 req = make_request ("get" , "http://python.org/" )
228- assert req .headers [" HOST" ] == "python.org"
228+ assert req .headers [hdrs . HOST ] == "python.org"
229229
230230
231231def test_host_header_host_with_default_port (make_request ) -> None :
232232 req = make_request ("get" , "http://python.org:80/" )
233- assert req .headers [" HOST" ] == "python.org"
233+ assert req .headers [hdrs . HOST ] == "python.org"
234234
235235
236236def test_host_header_host_with_nondefault_port (make_request ) -> None :
@@ -348,12 +348,12 @@ def test_skip_default_useragent_header(make_request) -> None:
348348
349349def test_headers (make_request ) -> None :
350350 req = make_request (
351- "post" , "http://python.org/" , headers = {"Content-Type" : "text/plain" }
351+ "post" , "http://python.org/" , headers = {hdrs . CONTENT_TYPE : "text/plain" }
352352 )
353353
354- assert "CONTENT-TYPE" in req .headers
355- assert req .headers ["CONTENT-TYPE" ] == "text/plain"
356- assert req .headers ["ACCEPT-ENCODING" ] == "gzip, deflate, br"
354+ assert hdrs . CONTENT_TYPE in req .headers
355+ assert req .headers [hdrs . CONTENT_TYPE ] == "text/plain"
356+ assert req .headers [hdrs . ACCEPT_ENCODING ] == "gzip, deflate, br"
357357
358358
359359def test_headers_list (make_request ) -> None :
@@ -979,7 +979,7 @@ async def test_body_with_size_sets_content_length(
979979async def test_body_payload_with_size_no_content_length (
980980 loop : asyncio .AbstractEventLoop ,
981981) -> None :
982- """Test that when a body payload with size is set directly , Content-Length is added."""
982+ """Test that when a body payload is set via update_body , Content-Length is added."""
983983 # Create a payload with a known size
984984 data = b"payload data"
985985 bytes_payload = payload .BytesPayload (data )
@@ -991,23 +991,28 @@ async def test_body_payload_with_size_no_content_length(
991991 loop = loop ,
992992 )
993993
994- # Set body directly (bypassing update_body_from_data to avoid it setting Content-Length)
995- req ._body = bytes_payload
996-
997- # Ensure conditions for the code path we want to test
998- assert req ._body is not None
999- assert hdrs .CONTENT_LENGTH not in req .headers
1000- assert req ._body .size is not None
1001- assert not req .chunked
994+ # Initially no body should be set
995+ assert req ._body is None
996+ # POST method with None body should have Content-Length: 0
997+ assert req .headers [hdrs .CONTENT_LENGTH ] == "0"
1002998
1003- # Now trigger update_transfer_encoding which should set Content-Length
1004- req .update_transfer_encoding ( )
999+ # Update body using the public method
1000+ await req .update_body ( bytes_payload )
10051001
10061002 # Verify Content-Length was set from body.size
1007- assert req .headers ["CONTENT-LENGTH" ] == str (len (data ))
1003+ assert req .headers [hdrs . CONTENT_LENGTH ] == str (len (data ))
10081004 assert req .body is bytes_payload
10091005 assert req ._body is bytes_payload # Access _body which is the Payload
1006+ assert req ._body is not None # type: ignore[unreachable]
10101007 assert req ._body .size == len (data )
1008+
1009+ # Set body back to None
1010+ await req .update_body (None )
1011+
1012+ # Verify Content-Length is back to 0 for POST with None body
1013+ assert req .headers [hdrs .CONTENT_LENGTH ] == "0"
1014+ assert req ._body is None
1015+
10111016 await req .close ()
10121017
10131018
@@ -1980,8 +1985,8 @@ async def test_update_body_updates_content_length(
19801985
19811986 # Clear body
19821987 await req .update_body (None )
1983- # For None body, Content-Length should not be set
1984- assert "Content-Length" not in req .headers
1988+ # For None body with POST method , Content-Length should be set to 0
1989+ assert req .headers [ hdrs . CONTENT_LENGTH ] == "0"
19851990
19861991 await req .close ()
19871992
@@ -2075,4 +2080,149 @@ async def test_expect100_with_body_becomes_none() -> None:
20752080 req ._body = None
20762081
20772082 await req .write_bytes (mock_writer , mock_conn , None )
2083+
2084+
2085+ @pytest .mark .parametrize (
2086+ ("method" , "data" , "expected_content_length" ),
2087+ [
2088+ # GET methods should not have Content-Length with None body
2089+ ("GET" , None , None ),
2090+ ("HEAD" , None , None ),
2091+ ("OPTIONS" , None , None ),
2092+ ("TRACE" , None , None ),
2093+ # POST methods should have Content-Length: 0 with None body
2094+ ("POST" , None , "0" ),
2095+ ("PUT" , None , "0" ),
2096+ ("PATCH" , None , "0" ),
2097+ ("DELETE" , None , "0" ),
2098+ # Empty bytes should always set Content-Length: 0
2099+ ("GET" , b"" , "0" ),
2100+ ("HEAD" , b"" , "0" ),
2101+ ("POST" , b"" , "0" ),
2102+ ("PUT" , b"" , "0" ),
2103+ # Non-empty bytes should set appropriate Content-Length
2104+ ("GET" , b"test" , "4" ),
2105+ ("POST" , b"test" , "4" ),
2106+ ("PUT" , b"hello world" , "11" ),
2107+ ("PATCH" , b"data" , "4" ),
2108+ ("DELETE" , b"x" , "1" ),
2109+ ],
2110+ )
2111+ def test_content_length_for_methods (
2112+ method : str ,
2113+ data : Optional [bytes ],
2114+ expected_content_length : Optional [str ],
2115+ loop : asyncio .AbstractEventLoop ,
2116+ ) -> None :
2117+ """Test that Content-Length header is set correctly for all HTTP methods."""
2118+ req = ClientRequest (method , URL ("http://python.org/" ), data = data , loop = loop )
2119+
2120+ actual_content_length = req .headers .get (hdrs .CONTENT_LENGTH )
2121+ assert actual_content_length == expected_content_length
2122+
2123+
2124+ @pytest .mark .parametrize ("method" , ["GET" , "HEAD" , "OPTIONS" , "TRACE" ])
2125+ def test_get_methods_classification (method : str ) -> None :
2126+ """Test that GET-like methods are correctly classified."""
2127+ assert method in ClientRequest .GET_METHODS
2128+
2129+
2130+ @pytest .mark .parametrize ("method" , ["POST" , "PUT" , "PATCH" , "DELETE" ])
2131+ def test_non_get_methods_classification (method : str ) -> None :
2132+ """Test that POST-like methods are not in GET_METHODS."""
2133+ assert method not in ClientRequest .GET_METHODS
2134+
2135+
2136+ async def test_content_length_with_string_data (loop : asyncio .AbstractEventLoop ) -> None :
2137+ """Test Content-Length when data is a string."""
2138+ data = "Hello, World!"
2139+ req = ClientRequest ("POST" , URL ("http://python.org/" ), data = data , loop = loop )
2140+ # String should be encoded to bytes, default encoding is utf-8
2141+ assert req .headers [hdrs .CONTENT_LENGTH ] == str (len (data .encode ("utf-8" )))
2142+ await req .close ()
2143+
2144+
2145+ async def test_content_length_with_async_iterable (
2146+ loop : asyncio .AbstractEventLoop ,
2147+ ) -> None :
2148+ """Test that async iterables use chunked encoding, not Content-Length."""
2149+
2150+ async def data_gen () -> AsyncIterator [bytes ]:
2151+ yield b"chunk1" # pragma: no cover
2152+
2153+ req = ClientRequest ("POST" , URL ("http://python.org/" ), data = data_gen (), loop = loop )
2154+ assert hdrs .CONTENT_LENGTH not in req .headers
2155+ assert req .chunked
2156+ assert req .headers [hdrs .TRANSFER_ENCODING ] == "chunked"
2157+ await req .close ()
2158+
2159+
2160+ async def test_content_length_not_overridden (loop : asyncio .AbstractEventLoop ) -> None :
2161+ """Test that explicitly set Content-Length is not overridden."""
2162+ req = ClientRequest (
2163+ "POST" ,
2164+ URL ("http://python.org/" ),
2165+ data = b"test" ,
2166+ headers = {hdrs .CONTENT_LENGTH : "100" },
2167+ loop = loop ,
2168+ )
2169+ # Should keep the explicitly set value
2170+ assert req .headers [hdrs .CONTENT_LENGTH ] == "100"
2171+ await req .close ()
2172+
2173+
2174+ async def test_content_length_with_formdata (loop : asyncio .AbstractEventLoop ) -> None :
2175+ """Test Content-Length with FormData."""
2176+ form = aiohttp .FormData ()
2177+ form .add_field ("field" , "value" )
2178+
2179+ req = ClientRequest ("POST" , URL ("http://python.org/" ), data = form , loop = loop )
2180+ # FormData with known size should set Content-Length
2181+ assert hdrs .CONTENT_LENGTH in req .headers
2182+ await req .close ()
2183+
2184+
2185+ async def test_no_content_length_with_chunked (loop : asyncio .AbstractEventLoop ) -> None :
2186+ """Test that chunked encoding prevents Content-Length header."""
2187+ req = ClientRequest (
2188+ "POST" ,
2189+ URL ("http://python.org/" ),
2190+ data = b"test" ,
2191+ chunked = True ,
2192+ loop = loop ,
2193+ )
2194+ assert hdrs .CONTENT_LENGTH not in req .headers
2195+ assert req .headers [hdrs .TRANSFER_ENCODING ] == "chunked"
2196+ await req .close ()
2197+
2198+
2199+ @pytest .mark .parametrize ("method" , ["POST" , "PUT" , "PATCH" , "DELETE" ])
2200+ async def test_update_body_none_sets_content_length_zero (
2201+ method : str , loop : asyncio .AbstractEventLoop
2202+ ) -> None :
2203+ """Test that updating body to None sets Content-Length: 0 for POST-like methods."""
2204+ # Create request with initial body
2205+ req = ClientRequest (method , URL ("http://python.org/" ), data = b"initial" , loop = loop )
2206+ assert req .headers [hdrs .CONTENT_LENGTH ] == "7"
2207+
2208+ # Update body to None
2209+ await req .update_body (None )
2210+ assert req .headers [hdrs .CONTENT_LENGTH ] == "0"
2211+ assert req ._body is None
2212+ await req .close ()
2213+
2214+
2215+ @pytest .mark .parametrize ("method" , ["GET" , "HEAD" , "OPTIONS" , "TRACE" ])
2216+ async def test_update_body_none_no_content_length_for_get_methods (
2217+ method : str , loop : asyncio .AbstractEventLoop
2218+ ) -> None :
2219+ """Test that updating body to None doesn't set Content-Length for GET-like methods."""
2220+ # Create request with initial body
2221+ req = ClientRequest (method , URL ("http://python.org/" ), data = b"initial" , loop = loop )
2222+ assert req .headers [hdrs .CONTENT_LENGTH ] == "7"
2223+
2224+ # Update body to None
2225+ await req .update_body (None )
2226+ assert hdrs .CONTENT_LENGTH not in req .headers
2227+ assert req ._body is None
20782228 await req .close ()
0 commit comments