1- use std:: fmt:: { Display , Formatter } ;
1+ use std:: fmt:: { Debug , Display , Formatter } ;
22use std:: ops:: Deref ;
33use std:: path:: PathBuf ;
44use std:: time:: { Duration , Instant } ;
@@ -514,9 +514,10 @@ impl ErrorKind {
514514///
515515/// Wraps a [`reqwest_middleware::Error`] instead of an [`reqwest::Error`] since the actual reqwest
516516/// error may be below some context in the [`anyhow::Error`].
517- #[ derive( Debug ) ]
517+ ///
518+ /// When displayed, URLs attached to the reqwest error are formatted through [`DisplaySafeUrl`].
518519pub struct WrappedReqwestError {
519- error : reqwest_middleware :: Error ,
520+ error : DisplaySafeReqwestMiddlewareError ,
520521 problem_details : Option < Box < ProblemDetails > > ,
521522}
522523
@@ -527,7 +528,7 @@ impl WrappedReqwestError {
527528 problem_details : Option < ProblemDetails > ,
528529 ) -> Self {
529530 Self {
530- error : Self :: filter_retries_from_error ( error) ,
531+ error : DisplaySafeReqwestMiddlewareError ( Self :: filter_retries_from_error ( error) ) ,
531532 problem_details : problem_details. map ( Box :: new) ,
532533 }
533534 }
@@ -554,7 +555,7 @@ impl WrappedReqwestError {
554555
555556 /// Return the inner [`reqwest::Error`] from the error chain, if it exists.
556557 pub fn inner ( & self ) -> Option < & reqwest:: Error > {
557- match & self . error {
558+ match & self . error . 0 {
558559 reqwest_middleware:: Error :: Reqwest ( err) => Some ( err) ,
559560 reqwest_middleware:: Error :: Middleware ( err) => err. chain ( ) . find_map ( |err| {
560561 if let Some ( err) = err. downcast_ref :: < reqwest:: Error > ( ) {
@@ -618,7 +619,7 @@ impl From<reqwest::Error> for WrappedReqwestError {
618619 fn from ( error : reqwest:: Error ) -> Self {
619620 Self {
620621 // No need to filter retries as this error does not have retries.
621- error : error. into ( ) ,
622+ error : DisplaySafeReqwestMiddlewareError ( error. into ( ) ) ,
622623 problem_details : None ,
623624 }
624625 }
@@ -627,7 +628,7 @@ impl From<reqwest::Error> for WrappedReqwestError {
627628impl From < reqwest_middleware:: Error > for WrappedReqwestError {
628629 fn from ( error : reqwest_middleware:: Error ) -> Self {
629630 Self {
630- error : Self :: filter_retries_from_error ( error) ,
631+ error : DisplaySafeReqwestMiddlewareError ( Self :: filter_retries_from_error ( error) ) ,
631632 problem_details : None ,
632633 }
633634 }
@@ -637,14 +638,14 @@ impl Deref for WrappedReqwestError {
637638 type Target = reqwest_middleware:: Error ;
638639
639640 fn deref ( & self ) -> & Self :: Target {
640- & self . error
641+ & self . error . 0
641642 }
642643}
643644
644645impl Display for WrappedReqwestError {
645646 fn fmt ( & self , f : & mut Formatter < ' _ > ) -> std:: fmt:: Result {
646647 if self . is_likely_offline ( ) {
647- // Insert an extra hint, we'll show the wrapped error through `source`
648+ // Insert an extra hint; the lower-level cause is still shown through `source`.
648649 f. write_str ( "Could not connect, are you offline?" )
649650 } else if let Some ( problem_details) = & self . problem_details {
650651 // Show problem details if available
@@ -662,22 +663,153 @@ impl Display for WrappedReqwestError {
662663impl std:: error:: Error for WrappedReqwestError {
663664 fn source ( & self ) -> Option < & ( dyn std:: error:: Error + ' static ) > {
664665 if self . is_likely_offline ( ) {
665- // `Display` is inserting an extra message, so we need to show the wrapped error
666+ // `Display` is inserting an extra message, so show the wrapped transport error.
666667 Some ( & self . error )
667668 } else if self . problem_details . is_some ( ) {
668- // `Display` is showing problem details, so show the wrapped error as source
669+ // `Display` is showing problem details, so show the wrapped transport error.
669670 Some ( & self . error )
670671 } else {
671- // `Display` is showing the wrapped error, continue with its source
672+ // `Display` is showing the wrapped error, continue with its source.
672673 self . error . source ( )
673674 }
674675 }
675676}
676677
678+ impl Debug for WrappedReqwestError {
679+ fn fmt ( & self , f : & mut Formatter < ' _ > ) -> std:: fmt:: Result {
680+ f. debug_struct ( "WrappedReqwestError" )
681+ . field ( "error" , & self . to_string ( ) )
682+ . field ( "problem_details" , & self . problem_details )
683+ . finish ( )
684+ }
685+ }
686+
687+ struct DisplaySafeReqwestMiddlewareError ( reqwest_middleware:: Error ) ;
688+
689+ impl Display for DisplaySafeReqwestMiddlewareError {
690+ fn fmt ( & self , f : & mut Formatter < ' _ > ) -> std:: fmt:: Result {
691+ display_reqwest_middleware_error ( & self . 0 , f)
692+ }
693+ }
694+
695+ impl Debug for DisplaySafeReqwestMiddlewareError {
696+ fn fmt ( & self , f : & mut Formatter < ' _ > ) -> std:: fmt:: Result {
697+ write ! ( f, "DisplaySafeReqwestMiddlewareError({self})" )
698+ }
699+ }
700+
701+ impl std:: error:: Error for DisplaySafeReqwestMiddlewareError {
702+ fn source ( & self ) -> Option < & ( dyn std:: error:: Error + ' static ) > {
703+ match & self . 0 {
704+ reqwest_middleware:: Error :: Middleware ( error) => error. source ( ) ,
705+ // Skip the outer reqwest error; its Display includes the unredacted URL.
706+ reqwest_middleware:: Error :: Reqwest ( error) => error. source ( ) ,
707+ }
708+ }
709+ }
710+
711+ fn display_reqwest_middleware_error (
712+ error : & reqwest_middleware:: Error ,
713+ f : & mut Formatter < ' _ > ,
714+ ) -> std:: fmt:: Result {
715+ match error {
716+ reqwest_middleware:: Error :: Middleware ( error) => write ! ( f, "{error}" ) ,
717+ reqwest_middleware:: Error :: Reqwest ( error) => display_reqwest_error ( error, f) ,
718+ }
719+ }
720+
721+ fn display_reqwest_error ( error : & reqwest:: Error , f : & mut Formatter < ' _ > ) -> std:: fmt:: Result {
722+ let Some ( url) = error. url ( ) else {
723+ return write ! ( f, "{error}" ) ;
724+ } ;
725+
726+ let message = error. to_string ( ) ;
727+ let raw_url = url. as_str ( ) ;
728+ let display_safe_url = DisplaySafeUrl :: ref_cast ( url) . to_string ( ) ;
729+ write ! ( f, "{}" , message. replace( raw_url, & display_safe_url) )
730+ }
731+
677732#[ cfg( test) ]
678733mod tests {
679734 use super :: * ;
680735
736+ use anyhow:: { Result , bail} ;
737+
738+ async fn rejected_signed_url_error ( ) -> Result < reqwest:: Error > {
739+ let result = reqwest:: Client :: new ( )
740+ . get ( "ftp://user:password@example.com/s3/dist.whl?X-Amz-Credential=credential-secret&X-Amz-Signature=signature-secret&X-Amz-Security-Token=token-secret" )
741+ . send ( )
742+ . await ;
743+ match result {
744+ Ok ( _) => bail ! ( "expected reqwest to reject the URL scheme" ) ,
745+ Err ( error) => Ok ( error) ,
746+ }
747+ }
748+
749+ #[ tokio:: test]
750+ async fn wrapped_reqwest_error_redacts_sensitive_url_parts ( ) -> Result < ( ) > {
751+ let error = WrappedReqwestError :: from ( rejected_signed_url_error ( ) . await ?) ;
752+
753+ assert_eq ! (
754+ error. to_string( ) ,
755+ "builder error for url (ftp://example.com/s3/dist.whl?X-Amz-Credential=****&X-Amz-Signature=****&X-Amz-Security-Token=****)"
756+ ) ;
757+
758+ let debug = format ! ( "{error:?}" ) ;
759+ for message in [ error. to_string ( ) , debug] {
760+ assert ! ( !message. contains( "credential-secret" ) ) ;
761+ assert ! ( !message. contains( "signature-secret" ) ) ;
762+ assert ! ( !message. contains( "token-secret" ) ) ;
763+ assert ! ( !message. contains( "password" ) ) ;
764+ }
765+
766+ let mut source = std:: error:: Error :: source ( & error) ;
767+ while let Some ( error) = source {
768+ let message = error. to_string ( ) ;
769+ assert ! ( !message. contains( "credential-secret" ) ) ;
770+ assert ! ( !message. contains( "signature-secret" ) ) ;
771+ assert ! ( !message. contains( "token-secret" ) ) ;
772+ assert ! ( !message. contains( "password" ) ) ;
773+ source = error. source ( ) ;
774+ }
775+
776+ Ok ( ( ) )
777+ }
778+
779+ #[ tokio:: test]
780+ async fn wrapped_reqwest_error_keeps_safe_source_with_problem_details ( ) -> Result < ( ) > {
781+ let error = WrappedReqwestError :: with_problem_details (
782+ rejected_signed_url_error ( ) . await ?. into ( ) ,
783+ Some ( ProblemDetails {
784+ problem_type : default_problem_type ( ) ,
785+ title : Some ( "problem title" . to_string ( ) ) ,
786+ detail : None ,
787+ status : Some ( 400 ) ,
788+ instance : None ,
789+ } ) ,
790+ ) ;
791+
792+ assert_eq ! ( error. to_string( ) , "Server message: problem title" ) ;
793+
794+ let source = std:: error:: Error :: source ( & error) . expect ( "source" ) ;
795+ assert_eq ! (
796+ source. to_string( ) ,
797+ "builder error for url (ftp://example.com/s3/dist.whl?X-Amz-Credential=****&X-Amz-Signature=****&X-Amz-Security-Token=****)"
798+ ) ;
799+
800+ let mut source = Some ( source) ;
801+ while let Some ( error) = source {
802+ let message = error. to_string ( ) ;
803+ assert ! ( !message. contains( "credential-secret" ) ) ;
804+ assert ! ( !message. contains( "signature-secret" ) ) ;
805+ assert ! ( !message. contains( "token-secret" ) ) ;
806+ assert ! ( !message. contains( "password" ) ) ;
807+ source = error. source ( ) ;
808+ }
809+
810+ Ok ( ( ) )
811+ }
812+
681813 #[ test]
682814 fn test_problem_details_parsing ( ) {
683815 let json = r#"{
0 commit comments