@@ -131,39 +131,26 @@ fn get_basic_auth(authority: &Authority) -> crate::Result<(Authority, Option<Aut
131131 // `http::Uri` accepts authorities that `url::Url` rejects (e.g. a
132132 // non-numeric port), so this parse can fail; propagate the error instead
133133 // of panicking.
134- let mut url = url:: Url :: parse ( & format ! ( "http://{authority}" ) ) ?;
135-
136- let user = url. username ( ) ;
137- if !user. is_empty ( ) {
138- let user = percent_decode_str ( user) . decode_utf8_lossy ( ) . into_owned ( ) ;
134+ let url = url:: Url :: parse ( & format ! ( "http://{authority}" ) ) ?;
135+ let Some ( ( _, host_port) ) = authority. as_str ( ) . rsplit_once ( '@' ) else {
136+ return Ok ( ( authority. clone ( ) , None ) ) ;
137+ } ;
139138
140- let password = url. password ( ) . unwrap_or ( "" ) ;
141- let password = percent_decode_str ( password)
139+ let has_auth = !url. username ( ) . is_empty ( ) || url. password ( ) . is_some ( ) ;
140+ let auth = has_auth. then ( || {
141+ let user = percent_decode_str ( url. username ( ) )
142142 . decode_utf8_lossy ( )
143143 . into_owned ( ) ;
144+ let password = percent_decode_str ( url. password ( ) . unwrap_or ( "" ) )
145+ . decode_utf8_lossy ( )
146+ . into_owned ( ) ;
147+ Auth :: Basic {
148+ user,
149+ password : password. into ( ) ,
150+ }
151+ } ) ;
144152
145- // These methods have the same failure condition as `username`,
146- // because we have a non-empty username, they cannot fail here.
147- url. set_username ( "" )
148- . map_err ( |_| "unexpected empty authority" ) ?;
149- url. set_password ( None )
150- . map_err ( |_| "unexpected empty authority" ) ?;
151-
152- let authority = Uri :: from_maybe_shared ( String :: from ( url) ) ?
153- . authority ( )
154- . ok_or_else ( || "unexpected empty authority" . to_string ( ) ) ?
155- . clone ( ) ;
156-
157- Ok ( (
158- authority,
159- Some ( Auth :: Basic {
160- user,
161- password : password. into ( ) ,
162- } ) ,
163- ) )
164- } else {
165- Ok ( ( authority. clone ( ) , None ) )
166- }
153+ Ok ( ( host_port. parse ( ) ?, auth) )
167154}
168155
169156/// Simplify the URI into a protocol and endpoint by removing the
@@ -497,6 +484,18 @@ mod tests {
497484 ) ;
498485
499486 test_parse ( "user@example.com" , "example.com" , Some ( ( "user" , "" ) ) ) ;
487+
488+ test_parse (
489+ "https://user:pass@example.com:80/api" ,
490+ "https://example.com:80/api" ,
491+ Some ( ( "user" , "pass" ) ) ,
492+ ) ;
493+
494+ test_parse (
495+ "https://:secret@example.com/api" ,
496+ "https://example.com/api" ,
497+ Some ( ( "" , "secret" ) ) ,
498+ ) ;
500499 }
501500
502501 #[ test]
@@ -563,6 +562,28 @@ mod tests {
563562 assert ! ( matches!( auth, Some ( Auth :: Basic { user, .. } ) if user == "user" ) ) ;
564563 }
565564
565+ #[ test]
566+ fn http_endpoint_auth_extraction_preserves_explicit_port ( ) {
567+ let endpoint = HttpEndpoint :: parse ( "user:pass@example.com:80/path" ) . unwrap ( ) ;
568+ let ( endpoint, auth) = endpoint. extract_basic_auth ( ) . unwrap ( ) ;
569+
570+ assert_eq ! ( endpoint. to_string( ) , "https://example.com:80/path" ) ;
571+ assert ! ( matches!( auth, Some ( Auth :: Basic { user, .. } ) if user == "user" ) ) ;
572+ }
573+
574+ #[ test]
575+ fn http_endpoint_auth_extraction_accepts_empty_username ( ) {
576+ let endpoint = HttpEndpoint :: parse ( ":secret@example.com/path" ) . unwrap ( ) ;
577+ let ( endpoint, auth) = endpoint. extract_basic_auth ( ) . unwrap ( ) ;
578+
579+ assert_eq ! ( endpoint. to_string( ) , "https://example.com/path" ) ;
580+ assert ! ( matches!(
581+ auth,
582+ Some ( Auth :: Basic { user, password } )
583+ if user. is_empty( ) && password. inner( ) == "secret"
584+ ) ) ;
585+ }
586+
566587 #[ test]
567588 fn http_endpoint_defaults_missing_scheme_to_https ( ) {
568589 for endpoint in [
0 commit comments