@@ -270,6 +270,7 @@ impl FullnodeClient {
270270 let bcs_bytes = signed_txn. to_bcs ( ) ?;
271271 let client = self . client . clone ( ) ;
272272 let retry_config = self . retry_config . clone ( ) ;
273+ let max_response_size = self . config . pool_config ( ) . max_response_size ;
273274
274275 let executor = RetryExecutor :: new ( ( * retry_config) . clone ( ) ) ;
275276 executor
@@ -286,7 +287,7 @@ impl FullnodeClient {
286287 . send ( )
287288 . await ?;
288289
289- Self :: handle_response_static ( response) . await
290+ Self :: handle_response_static ( response, max_response_size ) . await
290291 }
291292 } )
292293 . await
@@ -400,6 +401,7 @@ impl FullnodeClient {
400401 let bcs_bytes = signed_txn. to_bcs ( ) ?;
401402 let client = self . client . clone ( ) ;
402403 let retry_config = self . retry_config . clone ( ) ;
404+ let max_response_size = self . config . pool_config ( ) . max_response_size ;
403405
404406 let executor = RetryExecutor :: new ( ( * retry_config) . clone ( ) ) ;
405407 executor
@@ -416,7 +418,7 @@ impl FullnodeClient {
416418 . send ( )
417419 . await ?;
418420
419- Self :: handle_response_static ( response) . await
421+ Self :: handle_response_static ( response, max_response_size ) . await
420422 }
421423 } )
422424 . await
@@ -459,6 +461,7 @@ impl FullnodeClient {
459461
460462 let client = self . client . clone ( ) ;
461463 let retry_config = self . retry_config . clone ( ) ;
464+ let max_response_size = self . config . pool_config ( ) . max_response_size ;
462465
463466 let executor = RetryExecutor :: new ( ( * retry_config) . clone ( ) ) ;
464467 executor
@@ -475,7 +478,7 @@ impl FullnodeClient {
475478 . send ( )
476479 . await ?;
477480
478- Self :: handle_response_static ( response) . await
481+ Self :: handle_response_static ( response, max_response_size ) . await
479482 }
480483 } )
481484 . await
@@ -558,12 +561,17 @@ impl FullnodeClient {
558561 fn build_url ( & self , path : & str ) -> Url {
559562 let mut url = self . config . fullnode_url ( ) . clone ( ) ;
560563 if !path. is_empty ( ) {
561- // Ensure base path ends with /
562- if !url. path ( ) . ends_with ( '/' ) {
563- url. set_path ( & format ! ( "{}/" , url. path( ) ) ) ;
564+ // Avoid format! allocations by building the path string manually
565+ let base_path = url. path ( ) ;
566+ let needs_slash = !base_path. ends_with ( '/' ) ;
567+ let new_len = base_path. len ( ) + path. len ( ) + usize:: from ( needs_slash) ;
568+ let mut new_path = String :: with_capacity ( new_len) ;
569+ new_path. push_str ( base_path) ;
570+ if needs_slash {
571+ new_path. push ( '/' ) ;
564572 }
565- // Append the path segment
566- url. set_path ( & format ! ( "{}{}" , url . path ( ) , path ) ) ;
573+ new_path . push_str ( path) ;
574+ url. set_path ( & new_path ) ;
567575 }
568576 url
569577 }
@@ -575,6 +583,7 @@ impl FullnodeClient {
575583 let client = self . client . clone ( ) ;
576584 let url_clone = url. clone ( ) ;
577585 let retry_config = self . retry_config . clone ( ) ;
586+ let max_response_size = self . config . pool_config ( ) . max_response_size ;
578587
579588 let executor = RetryExecutor :: new ( ( * retry_config) . clone ( ) ) ;
580589 executor
@@ -588,18 +597,39 @@ impl FullnodeClient {
588597 . send ( )
589598 . await ?;
590599
591- Self :: handle_response_static ( response) . await
600+ Self :: handle_response_static ( response, max_response_size ) . await
592601 }
593602 } )
594603 . await
595604 }
596605
597606 /// Handles an HTTP response without retry (for internal use).
607+ ///
608+ /// # Security
609+ ///
610+ /// This method checks the Content-Length header against `max_response_size`
611+ /// to prevent memory exhaustion from extremely large responses.
598612 async fn handle_response_static < T : for < ' de > serde:: Deserialize < ' de > > (
599613 response : reqwest:: Response ,
614+ max_response_size : usize ,
600615 ) -> AptosResult < AptosResponse < T > > {
601616 let status = response. status ( ) ;
602617
618+ // SECURITY: Check Content-Length to prevent memory exhaustion
619+ // This protects against malicious servers sending extremely large responses
620+ if let Some ( content_length) = response. content_length ( )
621+ && content_length > max_response_size as u64
622+ {
623+ return Err ( AptosError :: Api {
624+ status_code : status. as_u16 ( ) ,
625+ message : format ! (
626+ "response body too large: {content_length} bytes exceeds limit of {max_response_size} bytes"
627+ ) ,
628+ error_code : Some ( "RESPONSE_TOO_LARGE" . to_string ( ) ) ,
629+ vm_error_code : None ,
630+ } ) ;
631+ }
632+
603633 // Extract headers before consuming response body
604634 let ledger_version = response
605635 . headers ( )
@@ -632,6 +662,13 @@ impl FullnodeClient {
632662 . and_then ( |v| v. to_str ( ) . ok ( ) )
633663 . map ( ToString :: to_string) ;
634664
665+ // Extract Retry-After header for rate limiting (before consuming body)
666+ let retry_after_secs = response
667+ . headers ( )
668+ . get ( "retry-after" )
669+ . and_then ( |v| v. to_str ( ) . ok ( ) )
670+ . and_then ( |v| v. parse ( ) . ok ( ) ) ;
671+
635672 if status. is_success ( ) {
636673 let data: T = response. json ( ) . await ?;
637674 Ok ( AptosResponse {
@@ -643,6 +680,10 @@ impl FullnodeClient {
643680 oldest_ledger_version,
644681 cursor,
645682 } )
683+ } else if status. as_u16 ( ) == 429 {
684+ // SECURITY: Return specific RateLimited error with Retry-After info
685+ // This allows callers to respect the server's rate limiting
686+ Err ( AptosError :: RateLimited { retry_after_secs } )
646687 } else {
647688 let body: serde_json:: Value = response. json ( ) . await . unwrap_or_default ( ) ;
648689 let message = body
@@ -673,7 +714,8 @@ impl FullnodeClient {
673714 & self ,
674715 response : reqwest:: Response ,
675716 ) -> AptosResult < AptosResponse < T > > {
676- Self :: handle_response_static ( response) . await
717+ let max_response_size = self . config . pool_config ( ) . max_response_size ;
718+ Self :: handle_response_static ( response, max_response_size) . await
677719 }
678720}
679721
0 commit comments