@@ -428,19 +428,12 @@ def _crt_request_from_aws_request(self, aws_request):
428428 headers_list .append ((name , str (value , 'utf-8' )))
429429
430430 crt_headers = awscrt .http .HttpHeaders (headers_list )
431- # CRT requires body (if it exists) to be an I/O stream.
432- crt_body_stream = None
433- if aws_request .body :
434- if hasattr (aws_request .body , 'seek' ):
435- crt_body_stream = aws_request .body
436- else :
437- crt_body_stream = BytesIO (aws_request .body )
438431
439432 crt_request = awscrt .http .HttpRequest (
440433 method = aws_request .method ,
441434 path = crt_path ,
442435 headers = crt_headers ,
443- body_stream = crt_body_stream ,
436+ body_stream = aws_request . body ,
444437 )
445438 return crt_request
446439
@@ -453,6 +446,25 @@ def _convert_to_crt_http_request(self, botocore_http_request):
453446 crt_request .headers .set ("host" , url_parts .netloc )
454447 if crt_request .headers .get ('Content-MD5' ) is not None :
455448 crt_request .headers .remove ("Content-MD5" )
449+
450+ # In general, the CRT S3 client expects a content length header. It
451+ # only expects a missing content length header if the body is not
452+ # seekable. However, botocore does not set the content length header
453+ # for GetObject API requests and so we set the content length to zero
454+ # to meet the CRT S3 client's expectation that the content length
455+ # header is set even if there is no body.
456+ if crt_request .headers .get ('Content-Length' ) is None :
457+ if botocore_http_request .body is None :
458+ crt_request .headers .add ('Content-Length' , "0" )
459+
460+ # Botocore sets the Transfer-Encoding header when it cannot determine
461+ # the content length of the request body (e.g. it's not seekable).
462+ # However, CRT does not support this header, but it supports
463+ # non-seekable bodies. So we remove this header to not cause issues
464+ # in the downstream CRT S3 request.
465+ if crt_request .headers .get ('Transfer-Encoding' ) is not None :
466+ crt_request .headers .remove ('Transfer-Encoding' )
467+
456468 return crt_request
457469
458470 def _capture_http_request (self , request , ** kwargs ):
@@ -555,39 +567,19 @@ def __init__(self, crt_request_serializer, os_utils):
555567 def get_make_request_args (
556568 self , request_type , call_args , coordinator , future , on_done_after_calls
557569 ):
558- recv_filepath = None
559- send_filepath = None
560- s3_meta_request_type = getattr (
561- S3RequestType , request_type .upper (), S3RequestType .DEFAULT
562- )
563- on_done_before_calls = []
564- if s3_meta_request_type == S3RequestType .GET_OBJECT :
565- final_filepath = call_args .fileobj
566- recv_filepath = self ._os_utils .get_temp_filename (final_filepath )
567- file_ondone_call = RenameTempFileHandler (
568- coordinator , final_filepath , recv_filepath , self ._os_utils
569- )
570- on_done_before_calls .append (file_ondone_call )
571- elif s3_meta_request_type == S3RequestType .PUT_OBJECT :
572- send_filepath = call_args .fileobj
573- data_len = self ._os_utils .get_file_size (send_filepath )
574- call_args .extra_args ["ContentLength" ] = data_len
575-
576- crt_request = self ._request_serializer .serialize_http_request (
577- request_type , future
570+ return getattr (
571+ self ,
572+ f'_get_make_request_args_{ request_type } ' ,
573+ self ._default_get_make_request_args ,
574+ )(
575+ request_type = request_type ,
576+ call_args = call_args ,
577+ coordinator = coordinator ,
578+ future = future ,
579+ on_done_before_calls = [],
580+ on_done_after_calls = on_done_after_calls ,
578581 )
579582
580- return {
581- 'request' : crt_request ,
582- 'type' : s3_meta_request_type ,
583- 'recv_filepath' : recv_filepath ,
584- 'send_filepath' : send_filepath ,
585- 'on_done' : self .get_crt_callback (
586- future , 'done' , on_done_before_calls , on_done_after_calls
587- ),
588- 'on_progress' : self .get_crt_callback (future , 'progress' ),
589- }
590-
591583 def get_crt_callback (
592584 self ,
593585 future ,
@@ -613,6 +605,96 @@ def invoke_all_callbacks(*args, **kwargs):
613605
614606 return invoke_all_callbacks
615607
608+ def _get_make_request_args_put_object (
609+ self ,
610+ request_type ,
611+ call_args ,
612+ coordinator ,
613+ future ,
614+ on_done_before_calls ,
615+ on_done_after_calls ,
616+ ):
617+ send_filepath = None
618+ if isinstance (call_args .fileobj , str ):
619+ send_filepath = call_args .fileobj
620+ data_len = self ._os_utils .get_file_size (send_filepath )
621+ call_args .extra_args ["ContentLength" ] = data_len
622+ else :
623+ call_args .extra_args ["Body" ] = call_args .fileobj
624+
625+ # Suppress botocore's automatic MD5 calculation by setting an override
626+ # value that will get deleted in the BotocoreCRTRequestSerializer.
627+ # The CRT S3 client will automatically compute checksums as part of
628+ # requests it makes.
629+ call_args .extra_args ["ContentMD5" ] = "override-to-be-removed"
630+
631+ make_request_args = self ._default_get_make_request_args (
632+ request_type = request_type ,
633+ call_args = call_args ,
634+ coordinator = coordinator ,
635+ future = future ,
636+ on_done_before_calls = on_done_before_calls ,
637+ on_done_after_calls = on_done_after_calls ,
638+ )
639+ make_request_args ['send_filepath' ] = send_filepath
640+ return make_request_args
641+
642+ def _get_make_request_args_get_object (
643+ self ,
644+ request_type ,
645+ call_args ,
646+ coordinator ,
647+ future ,
648+ on_done_before_calls ,
649+ on_done_after_calls ,
650+ ):
651+ recv_filepath = None
652+ on_body = None
653+ if isinstance (call_args .fileobj , str ):
654+ final_filepath = call_args .fileobj
655+ recv_filepath = self ._os_utils .get_temp_filename (final_filepath )
656+ on_done_before_calls .append (
657+ RenameTempFileHandler (
658+ coordinator , final_filepath , recv_filepath , self ._os_utils
659+ )
660+ )
661+ else :
662+ on_body = OnBodyFileObjWriter (call_args .fileobj )
663+
664+ make_request_args = self ._default_get_make_request_args (
665+ request_type = request_type ,
666+ call_args = call_args ,
667+ coordinator = coordinator ,
668+ future = future ,
669+ on_done_before_calls = on_done_before_calls ,
670+ on_done_after_calls = on_done_after_calls ,
671+ )
672+ make_request_args ['recv_filepath' ] = recv_filepath
673+ make_request_args ['on_body' ] = on_body
674+ return make_request_args
675+
676+ def _default_get_make_request_args (
677+ self ,
678+ request_type ,
679+ call_args ,
680+ coordinator ,
681+ future ,
682+ on_done_before_calls ,
683+ on_done_after_calls ,
684+ ):
685+ return {
686+ 'request' : self ._request_serializer .serialize_http_request (
687+ request_type , future
688+ ),
689+ 'type' : getattr (
690+ S3RequestType , request_type .upper (), S3RequestType .DEFAULT
691+ ),
692+ 'on_done' : self .get_crt_callback (
693+ future , 'done' , on_done_before_calls , on_done_after_calls
694+ ),
695+ 'on_progress' : self .get_crt_callback (future , 'progress' ),
696+ }
697+
616698
617699class RenameTempFileHandler :
618700 def __init__ (self , coordinator , final_filename , temp_filename , osutil ):
@@ -642,3 +724,11 @@ def __init__(self, coordinator):
642724
643725 def __call__ (self , ** kwargs ):
644726 self ._coordinator .set_done_callbacks_complete ()
727+
728+
729+ class OnBodyFileObjWriter :
730+ def __init__ (self , fileobj ):
731+ self ._fileobj = fileobj
732+
733+ def __call__ (self , chunk , ** kwargs ):
734+ self ._fileobj .write (chunk )
0 commit comments