@@ -370,6 +370,23 @@ def __init__(
370
370
def __reset_writer (self , _ : object = None ) -> None :
371
371
self .__writer = None
372
372
373
+ def _get_content_length (self ) -> Optional [int ]:
374
+ """Extract and validate Content-Length header value.
375
+
376
+ Returns parsed Content-Length value or None if not set.
377
+ Raises ValueError if header exists but cannot be parsed as an integer.
378
+ """
379
+ if hdrs .CONTENT_LENGTH not in self .headers :
380
+ return None
381
+
382
+ content_length_hdr = self .headers [hdrs .CONTENT_LENGTH ]
383
+ try :
384
+ return int (content_length_hdr )
385
+ except ValueError :
386
+ raise ValueError (
387
+ f"Invalid Content-Length header: { content_length_hdr } "
388
+ ) from None
389
+
373
390
@property
374
391
def skip_auto_headers (self ) -> CIMultiDict [None ]:
375
392
return self ._skip_auto_headers or CIMultiDict ()
@@ -659,9 +676,37 @@ def update_proxy(
659
676
self .proxy_headers = proxy_headers
660
677
661
678
async def write_bytes (
662
- self , writer : AbstractStreamWriter , conn : "Connection"
679
+ self ,
680
+ writer : AbstractStreamWriter ,
681
+ conn : "Connection" ,
682
+ content_length : Optional [int ],
663
683
) -> None :
664
- """Support coroutines that yields bytes objects."""
684
+ """
685
+ Write the request body to the connection stream.
686
+
687
+ This method handles writing different types of request bodies:
688
+ 1. Payload objects (using their specialized write_with_length method)
689
+ 2. Bytes/bytearray objects
690
+ 3. Iterable body content
691
+
692
+ Args:
693
+ writer: The stream writer to write the body to
694
+ conn: The connection being used for this request
695
+ content_length: Optional maximum number of bytes to write from the body
696
+ (None means write the entire body)
697
+
698
+ The method properly handles:
699
+ - Waiting for 100-Continue responses if required
700
+ - Content length constraints for chunked encoding
701
+ - Error handling for network issues, cancellation, and other exceptions
702
+ - Signaling EOF and timeout management
703
+
704
+ Raises:
705
+ ClientOSError: When there's an OS-level error writing the body
706
+ ClientConnectionError: When there's a general connection error
707
+ asyncio.CancelledError: When the operation is cancelled
708
+
709
+ """
665
710
# 100 response
666
711
if self ._continue is not None :
667
712
await writer .drain ()
@@ -671,16 +716,30 @@ async def write_bytes(
671
716
assert protocol is not None
672
717
try :
673
718
if isinstance (self .body , payload .Payload ):
674
- await self .body .write (writer )
719
+ # Specialized handling for Payload objects that know how to write themselves
720
+ await self .body .write_with_length (writer , content_length )
675
721
else :
722
+ # Handle bytes/bytearray by converting to an iterable for consistent handling
676
723
if isinstance (self .body , (bytes , bytearray )):
677
724
self .body = (self .body ,)
678
725
679
- for chunk in self .body :
680
- await writer .write (chunk )
726
+ if content_length is None :
727
+ # Write the entire body without length constraint
728
+ for chunk in self .body :
729
+ await writer .write (chunk )
730
+ else :
731
+ # Write with length constraint, respecting content_length limit
732
+ # If the body is larger than content_length, we truncate it
733
+ remaining_bytes = content_length
734
+ for chunk in self .body :
735
+ await writer .write (chunk [:remaining_bytes ])
736
+ remaining_bytes -= len (chunk )
737
+ if remaining_bytes <= 0 :
738
+ break
681
739
except OSError as underlying_exc :
682
740
reraised_exc = underlying_exc
683
741
742
+ # Distinguish between timeout and other OS errors for better error reporting
684
743
exc_is_not_timeout = underlying_exc .errno is not None or not isinstance (
685
744
underlying_exc , asyncio .TimeoutError
686
745
)
@@ -692,18 +751,20 @@ async def write_bytes(
692
751
693
752
set_exception (protocol , reraised_exc , underlying_exc )
694
753
except asyncio .CancelledError :
695
- # Body hasn't been fully sent, so connection can't be reused.
754
+ # Body hasn't been fully sent, so connection can't be reused
696
755
conn .close ()
697
756
raise
698
757
except Exception as underlying_exc :
699
758
set_exception (
700
759
protocol ,
701
760
ClientConnectionError (
702
- f"Failed to send bytes into the underlying connection { conn !s} " ,
761
+ "Failed to send bytes into the underlying connection "
762
+ f"{ conn !s} : { underlying_exc !r} " ,
703
763
),
704
764
underlying_exc ,
705
765
)
706
766
else :
767
+ # Successfully wrote the body, signal EOF and start response timeout
707
768
await writer .write_eof ()
708
769
protocol .start_timeout ()
709
770
@@ -768,7 +829,7 @@ async def send(self, conn: "Connection") -> "ClientResponse":
768
829
await writer .write_headers (status_line , self .headers )
769
830
task : Optional ["asyncio.Task[None]" ]
770
831
if self .body or self ._continue is not None or protocol .writing_paused :
771
- coro = self .write_bytes (writer , conn )
832
+ coro = self .write_bytes (writer , conn , self . _get_content_length () )
772
833
if sys .version_info >= (3 , 12 ):
773
834
# Optimization for Python 3.12, try to write
774
835
# bytes immediately to avoid having to schedule
0 commit comments