@@ -217,7 +217,7 @@ def test_host_port_nondefault_wss(make_request: _RequestMaker) -> None:
217217
218218def test_host_port_none_port (make_request : _RequestMaker ) -> None :
219219 req = make_request ("get" , "unix://localhost/path" )
220- assert req .headers ["Host" ] == "localhost"
220+ assert req .headers [hdrs . HOST ] == "localhost"
221221
222222
223223def test_host_port_err (make_request : _RequestMaker ) -> None :
@@ -232,17 +232,17 @@ def test_hostname_err(make_request: _RequestMaker) -> None:
232232
233233def test_host_header_host_first (make_request : _RequestMaker ) -> None :
234234 req = make_request ("get" , "http://python.org/" )
235- assert list (req .headers )[0 ] == "Host"
235+ assert list (req .headers )[0 ] == hdrs . HOST
236236
237237
238238def test_host_header_host_without_port (make_request : _RequestMaker ) -> None :
239239 req = make_request ("get" , "http://python.org/" )
240- assert req .headers [" HOST" ] == "python.org"
240+ assert req .headers [hdrs . HOST ] == "python.org"
241241
242242
243243def test_host_header_host_with_default_port (make_request : _RequestMaker ) -> None :
244244 req = make_request ("get" , "http://python.org:80/" )
245- assert req .headers [" HOST" ] == "python.org"
245+ assert req .headers [hdrs . HOST ] == "python.org"
246246
247247
248248def test_host_header_host_with_nondefault_port (make_request : _RequestMaker ) -> None :
@@ -353,12 +353,12 @@ def test_skip_default_useragent_header(make_request: _RequestMaker) -> None:
353353
354354def test_headers (make_request : _RequestMaker ) -> None :
355355 req = make_request (
356- "post" , "http://python.org/" , headers = {"Content-Type" : "text/plain" }
356+ "post" , "http://python.org/" , headers = {hdrs . CONTENT_TYPE : "text/plain" }
357357 )
358358
359- assert "CONTENT-TYPE" in req .headers
360- assert req .headers ["CONTENT-TYPE" ] == "text/plain"
361- assert req .headers ["ACCEPT-ENCODING" ] == "gzip, deflate, br"
359+ assert hdrs . CONTENT_TYPE in req .headers
360+ assert req .headers [hdrs . CONTENT_TYPE ] == "text/plain"
361+ assert req .headers [hdrs . ACCEPT_ENCODING ] == "gzip, deflate, br"
362362
363363
364364def test_headers_list (make_request : _RequestMaker ) -> None :
@@ -1034,7 +1034,7 @@ async def test_body_with_size_sets_content_length(
10341034async def test_body_payload_with_size_no_content_length (
10351035 loop : asyncio .AbstractEventLoop ,
10361036) -> None :
1037- """Test that when a body payload with size is set directly , Content-Length is added."""
1037+ """Test that when a body payload is set via update_body , Content-Length is added."""
10381038 # Create a payload with a known size
10391039 data = b"payload data"
10401040 bytes_payload = payload .BytesPayload (data )
@@ -1046,23 +1046,28 @@ async def test_body_payload_with_size_no_content_length(
10461046 loop = loop ,
10471047 )
10481048
1049- # Set body directly (bypassing update_body_from_data to avoid it setting Content-Length)
1050- req ._body = bytes_payload
1051-
1052- # Ensure conditions for the code path we want to test
1053- assert req ._body is not None
1054- assert hdrs .CONTENT_LENGTH not in req .headers
1055- assert req ._body .size is not None
1056- assert not req .chunked
1049+ # Initially no body should be set
1050+ assert req ._body is None
1051+ # POST method with None body should have Content-Length: 0
1052+ assert req .headers [hdrs .CONTENT_LENGTH ] == "0"
10571053
1058- # Now trigger update_transfer_encoding which should set Content-Length
1059- req .update_transfer_encoding ( )
1054+ # Update body using the public method
1055+ await req .update_body ( bytes_payload )
10601056
10611057 # Verify Content-Length was set from body.size
1062- assert req .headers ["CONTENT-LENGTH" ] == str (len (data ))
1058+ assert req .headers [hdrs . CONTENT_LENGTH ] == str (len (data ))
10631059 assert req .body is bytes_payload
10641060 assert req ._body is bytes_payload # Access _body which is the Payload
1061+ assert req ._body is not None # type: ignore[unreachable]
10651062 assert req ._body .size == len (data )
1063+
1064+ # Set body back to None
1065+ await req .update_body (None )
1066+
1067+ # Verify Content-Length is back to 0 for POST with None body
1068+ assert req .headers [hdrs .CONTENT_LENGTH ] == "0"
1069+ assert req ._body is None
1070+
10661071 await req .close ()
10671072
10681073
@@ -2032,8 +2037,8 @@ async def test_update_body_updates_content_length(
20322037
20332038 # Clear body
20342039 await req .update_body (None )
2035- # For None body, Content-Length should not be set
2036- assert "Content-Length" not in req .headers
2040+ # For None body with POST method , Content-Length should be set to 0
2041+ assert req .headers [ hdrs . CONTENT_LENGTH ] == "0"
20372042
20382043 await req .close ()
20392044
@@ -2127,4 +2132,149 @@ async def test_expect100_with_body_becomes_none() -> None:
21272132 req ._body = None
21282133
21292134 await req .write_bytes (mock_writer , mock_conn , None )
2135+
2136+
2137+ @pytest .mark .parametrize (
2138+ ("method" , "data" , "expected_content_length" ),
2139+ [
2140+ # GET methods should not have Content-Length with None body
2141+ ("GET" , None , None ),
2142+ ("HEAD" , None , None ),
2143+ ("OPTIONS" , None , None ),
2144+ ("TRACE" , None , None ),
2145+ # POST methods should have Content-Length: 0 with None body
2146+ ("POST" , None , "0" ),
2147+ ("PUT" , None , "0" ),
2148+ ("PATCH" , None , "0" ),
2149+ ("DELETE" , None , "0" ),
2150+ # Empty bytes should always set Content-Length: 0
2151+ ("GET" , b"" , "0" ),
2152+ ("HEAD" , b"" , "0" ),
2153+ ("POST" , b"" , "0" ),
2154+ ("PUT" , b"" , "0" ),
2155+ # Non-empty bytes should set appropriate Content-Length
2156+ ("GET" , b"test" , "4" ),
2157+ ("POST" , b"test" , "4" ),
2158+ ("PUT" , b"hello world" , "11" ),
2159+ ("PATCH" , b"data" , "4" ),
2160+ ("DELETE" , b"x" , "1" ),
2161+ ],
2162+ )
2163+ def test_content_length_for_methods (
2164+ method : str ,
2165+ data : Optional [bytes ],
2166+ expected_content_length : Optional [str ],
2167+ loop : asyncio .AbstractEventLoop ,
2168+ ) -> None :
2169+ """Test that Content-Length header is set correctly for all HTTP methods."""
2170+ req = ClientRequest (method , URL ("http://python.org/" ), data = data , loop = loop )
2171+
2172+ actual_content_length = req .headers .get (hdrs .CONTENT_LENGTH )
2173+ assert actual_content_length == expected_content_length
2174+
2175+
2176+ @pytest .mark .parametrize ("method" , ["GET" , "HEAD" , "OPTIONS" , "TRACE" ])
2177+ def test_get_methods_classification (method : str ) -> None :
2178+ """Test that GET-like methods are correctly classified."""
2179+ assert method in ClientRequest .GET_METHODS
2180+
2181+
2182+ @pytest .mark .parametrize ("method" , ["POST" , "PUT" , "PATCH" , "DELETE" ])
2183+ def test_non_get_methods_classification (method : str ) -> None :
2184+ """Test that POST-like methods are not in GET_METHODS."""
2185+ assert method not in ClientRequest .GET_METHODS
2186+
2187+
2188+ async def test_content_length_with_string_data (loop : asyncio .AbstractEventLoop ) -> None :
2189+ """Test Content-Length when data is a string."""
2190+ data = "Hello, World!"
2191+ req = ClientRequest ("POST" , URL ("http://python.org/" ), data = data , loop = loop )
2192+ # String should be encoded to bytes, default encoding is utf-8
2193+ assert req .headers [hdrs .CONTENT_LENGTH ] == str (len (data .encode ("utf-8" )))
2194+ await req .close ()
2195+
2196+
2197+ async def test_content_length_with_async_iterable (
2198+ loop : asyncio .AbstractEventLoop ,
2199+ ) -> None :
2200+ """Test that async iterables use chunked encoding, not Content-Length."""
2201+
2202+ async def data_gen () -> AsyncIterator [bytes ]:
2203+ yield b"chunk1" # pragma: no cover
2204+
2205+ req = ClientRequest ("POST" , URL ("http://python.org/" ), data = data_gen (), loop = loop )
2206+ assert hdrs .CONTENT_LENGTH not in req .headers
2207+ assert req .chunked
2208+ assert req .headers [hdrs .TRANSFER_ENCODING ] == "chunked"
2209+ await req .close ()
2210+
2211+
2212+ async def test_content_length_not_overridden (loop : asyncio .AbstractEventLoop ) -> None :
2213+ """Test that explicitly set Content-Length is not overridden."""
2214+ req = ClientRequest (
2215+ "POST" ,
2216+ URL ("http://python.org/" ),
2217+ data = b"test" ,
2218+ headers = {hdrs .CONTENT_LENGTH : "100" },
2219+ loop = loop ,
2220+ )
2221+ # Should keep the explicitly set value
2222+ assert req .headers [hdrs .CONTENT_LENGTH ] == "100"
2223+ await req .close ()
2224+
2225+
2226+ async def test_content_length_with_formdata (loop : asyncio .AbstractEventLoop ) -> None :
2227+ """Test Content-Length with FormData."""
2228+ form = aiohttp .FormData ()
2229+ form .add_field ("field" , "value" )
2230+
2231+ req = ClientRequest ("POST" , URL ("http://python.org/" ), data = form , loop = loop )
2232+ # FormData with known size should set Content-Length
2233+ assert hdrs .CONTENT_LENGTH in req .headers
2234+ await req .close ()
2235+
2236+
2237+ async def test_no_content_length_with_chunked (loop : asyncio .AbstractEventLoop ) -> None :
2238+ """Test that chunked encoding prevents Content-Length header."""
2239+ req = ClientRequest (
2240+ "POST" ,
2241+ URL ("http://python.org/" ),
2242+ data = b"test" ,
2243+ chunked = True ,
2244+ loop = loop ,
2245+ )
2246+ assert hdrs .CONTENT_LENGTH not in req .headers
2247+ assert req .headers [hdrs .TRANSFER_ENCODING ] == "chunked"
2248+ await req .close ()
2249+
2250+
2251+ @pytest .mark .parametrize ("method" , ["POST" , "PUT" , "PATCH" , "DELETE" ])
2252+ async def test_update_body_none_sets_content_length_zero (
2253+ method : str , loop : asyncio .AbstractEventLoop
2254+ ) -> None :
2255+ """Test that updating body to None sets Content-Length: 0 for POST-like methods."""
2256+ # Create request with initial body
2257+ req = ClientRequest (method , URL ("http://python.org/" ), data = b"initial" , loop = loop )
2258+ assert req .headers [hdrs .CONTENT_LENGTH ] == "7"
2259+
2260+ # Update body to None
2261+ await req .update_body (None )
2262+ assert req .headers [hdrs .CONTENT_LENGTH ] == "0"
2263+ assert req ._body is None
2264+ await req .close ()
2265+
2266+
2267+ @pytest .mark .parametrize ("method" , ["GET" , "HEAD" , "OPTIONS" , "TRACE" ])
2268+ async def test_update_body_none_no_content_length_for_get_methods (
2269+ method : str , loop : asyncio .AbstractEventLoop
2270+ ) -> None :
2271+ """Test that updating body to None doesn't set Content-Length for GET-like methods."""
2272+ # Create request with initial body
2273+ req = ClientRequest (method , URL ("http://python.org/" ), data = b"initial" , loop = loop )
2274+ assert req .headers [hdrs .CONTENT_LENGTH ] == "7"
2275+
2276+ # Update body to None
2277+ await req .update_body (None )
2278+ assert hdrs .CONTENT_LENGTH not in req .headers
2279+ assert req ._body is None
21302280 await req .close ()
0 commit comments