@@ -113,12 +113,11 @@ impl Binary {
113113 . unwrap( ) ,
114114 DisplaySafeUrl :: parse( & format!( "{VERSIONS_MANIFEST_URL}/{name}.ndjson" ) ) . unwrap( ) ,
115115 ] ,
116- Self :: Uv => {
117- vec ! [
118- DisplaySafeUrl :: parse( & format!( "{VERSIONS_MANIFEST_URL}/{name}.ndjson" ) )
119- . unwrap( ) ,
120- ]
121- }
116+ Self :: Uv => vec ! [
117+ DisplaySafeUrl :: parse( & format!( "{VERSIONS_MANIFEST_MIRROR}/{name}.ndjson" ) )
118+ . unwrap( ) ,
119+ DisplaySafeUrl :: parse( & format!( "{VERSIONS_MANIFEST_URL}/{name}.ndjson" ) ) . unwrap( ) ,
120+ ] ,
122121 }
123122 }
124123
@@ -393,11 +392,13 @@ impl RetriableError for Error {
393392
394393 /// Returns `true` if trying an alternative URL makes sense after this error.
395394 ///
396- /// All errors arising from downloading (including streaming during extraction)
397- /// qualify.
395+ /// Download and streaming failures qualify, as do malformed manifest responses.
398396 fn should_try_next_url ( & self ) -> bool {
399397 match self {
400- Self :: Download { .. } | Self :: ManifestFetch { .. } => true ,
398+ Self :: Download { .. }
399+ | Self :: ManifestFetch { .. }
400+ | Self :: ManifestParse ( ..)
401+ | Self :: ManifestUtf8 ( ..) => true ,
401402 Self :: Stream { .. } => true ,
402403 Self :: RetriedError { err, .. } => err. should_try_next_url ( ) ,
403404 err => {
@@ -856,13 +857,98 @@ where
856857
857858#[ cfg( test) ]
858859mod tests {
859- use std:: io:: Write ;
860+ use std:: io:: { Read , Write } ;
860861 use std:: net:: TcpListener ;
861- use uv_client:: { BaseClientBuilder , retryable_on_request_failure} ;
862+ use std:: sync:: Arc ;
863+ use std:: sync:: atomic:: { AtomicUsize , Ordering } ;
864+ use std:: sync:: mpsc:: { self , Sender } ;
865+ use std:: thread:: JoinHandle ;
866+ use std:: time:: Duration ;
867+ use uv_client:: { BaseClientBuilder , fetch_with_url_fallback, retryable_on_request_failure} ;
862868 use uv_redacted:: DisplaySafeUrl ;
863869
864870 use super :: * ;
865871
872+ fn spawn_http_server (
873+ response : String ,
874+ ) -> ( DisplaySafeUrl , Arc < AtomicUsize > , Sender < ( ) > , JoinHandle < ( ) > ) {
875+ let listener = TcpListener :: bind ( "127.0.0.1:0" ) . unwrap ( ) ;
876+ listener. set_nonblocking ( true ) . unwrap ( ) ;
877+ let addr = listener. local_addr ( ) . unwrap ( ) ;
878+ let requests = Arc :: new ( AtomicUsize :: new ( 0 ) ) ;
879+ let requests_clone = Arc :: clone ( & requests) ;
880+ let ( shutdown_tx, shutdown_rx) = mpsc:: channel ( ) ;
881+ let handle = std:: thread:: spawn ( move || {
882+ loop {
883+ if shutdown_rx. try_recv ( ) . is_ok ( ) {
884+ return ;
885+ }
886+
887+ match listener. accept ( ) {
888+ Ok ( ( mut stream, _) ) => {
889+ requests_clone. fetch_add ( 1 , Ordering :: SeqCst ) ;
890+ // Drain the request; we don't inspect it in these tests.
891+ let mut buf = [ 0u8 ; 4096 ] ;
892+ let _ = stream. read ( & mut buf) ;
893+ stream. write_all ( response. as_bytes ( ) ) . unwrap ( ) ;
894+ return ;
895+ }
896+ Err ( err) if err. kind ( ) == io:: ErrorKind :: WouldBlock => {
897+ std:: thread:: sleep ( Duration :: from_millis ( 10 ) ) ;
898+ }
899+ Err ( err) => panic ! ( "failed to accept connection: {err}" ) ,
900+ }
901+ }
902+ } ) ;
903+ (
904+ DisplaySafeUrl :: parse ( & format ! ( "http://{addr}/uv.ndjson" ) ) . unwrap ( ) ,
905+ requests,
906+ shutdown_tx,
907+ handle,
908+ )
909+ }
910+
911+ fn manifest_response ( body : & str ) -> String {
912+ format ! (
913+ "HTTP/1.1 200 OK\r \n Content-Length: {}\r \n Content-Type: application/x-ndjson\r \n \r \n {body}" ,
914+ body. len( )
915+ )
916+ }
917+
918+ fn not_found_response ( ) -> String {
919+ "HTTP/1.1 404 Not Found\r \n Content-Length: 0\r \n \r \n " . to_string ( )
920+ }
921+
922+ fn uv_manifest_line ( version : & str , platform : & str ) -> String {
923+ let extension = if cfg ! ( windows) { "zip" } else { "tar.gz" } ;
924+ format ! (
925+ "{{\" version\" :\" {version}\" ,\" date\" :\" 2025-01-01T00:00:00Z\" ,\" artifacts\" :[{{\" platform\" :\" {platform}\" ,\" url\" :\" https://github.com/astral-sh/uv/releases/download/{version}/uv-{platform}.{extension}\" ,\" archive_format\" :\" {extension}\" }}]}}\n "
926+ )
927+ }
928+
929+ async fn resolve_version_from_manifest_urls (
930+ urls : & [ DisplaySafeUrl ] ,
931+ constraints : Option < & VersionSpecifiers > ,
932+ ) -> Result < ResolvedVersion , Error > {
933+ let platform = Platform :: from_env ( ) . unwrap ( ) ;
934+ let platform_name = platform. as_cargo_dist_triple ( ) ;
935+ let client_builder = BaseClientBuilder :: default ( ) . retries ( 0 ) ;
936+ let retry_policy = client_builder. retry_policy ( ) ;
937+ let client = client_builder. build ( ) ;
938+
939+ fetch_with_url_fallback ( urls, retry_policy, "manifest for `uv`" , |url| {
940+ fetch_and_find_matching_version (
941+ Binary :: Uv ,
942+ constraints,
943+ None ,
944+ & platform_name,
945+ url,
946+ & client,
947+ )
948+ } )
949+ . await
950+ }
951+
866952 #[ test]
867953 fn test_uv_download_urls ( ) {
868954 let urls = Binary :: Uv
@@ -886,6 +972,88 @@ mod tests {
886972 ) ;
887973 }
888974
975+ #[ tokio:: test]
976+ async fn test_manifest_falls_back_on_404 ( ) {
977+ let platform = Platform :: from_env ( ) . unwrap ( ) ;
978+ let platform_name = platform. as_cargo_dist_triple ( ) ;
979+ let ( mirror_url, mirror_requests, mirror_shutdown, mirror_handle) =
980+ spawn_http_server ( not_found_response ( ) ) ;
981+ let ( canonical_url, canonical_requests, canonical_shutdown, canonical_handle) =
982+ spawn_http_server ( manifest_response ( & uv_manifest_line (
983+ "1.2.3" ,
984+ & platform_name,
985+ ) ) ) ;
986+
987+ let resolved = resolve_version_from_manifest_urls ( & [ mirror_url, canonical_url] , None )
988+ . await
989+ . expect ( "404 from mirror should fall back to canonical manifest" ) ;
990+
991+ let _ = mirror_shutdown. send ( ( ) ) ;
992+ let _ = canonical_shutdown. send ( ( ) ) ;
993+ mirror_handle. join ( ) . unwrap ( ) ;
994+ canonical_handle. join ( ) . unwrap ( ) ;
995+
996+ assert_eq ! ( resolved. version, Version :: new( [ 1 , 2 , 3 ] ) ) ;
997+ assert_eq ! ( mirror_requests. load( Ordering :: SeqCst ) , 1 ) ;
998+ assert_eq ! ( canonical_requests. load( Ordering :: SeqCst ) , 1 ) ;
999+ }
1000+
1001+ #[ tokio:: test]
1002+ async fn test_manifest_falls_back_on_parse_error ( ) {
1003+ let platform = Platform :: from_env ( ) . unwrap ( ) ;
1004+ let platform_name = platform. as_cargo_dist_triple ( ) ;
1005+ let ( mirror_url, mirror_requests, mirror_shutdown, mirror_handle) =
1006+ spawn_http_server ( manifest_response ( "{not json}\n " ) ) ;
1007+ let ( canonical_url, canonical_requests, canonical_shutdown, canonical_handle) =
1008+ spawn_http_server ( manifest_response ( & uv_manifest_line (
1009+ "1.2.3" ,
1010+ & platform_name,
1011+ ) ) ) ;
1012+
1013+ let resolved = resolve_version_from_manifest_urls ( & [ mirror_url, canonical_url] , None )
1014+ . await
1015+ . expect ( "parse failure from mirror should fall back to canonical manifest" ) ;
1016+
1017+ let _ = mirror_shutdown. send ( ( ) ) ;
1018+ let _ = canonical_shutdown. send ( ( ) ) ;
1019+ mirror_handle. join ( ) . unwrap ( ) ;
1020+ canonical_handle. join ( ) . unwrap ( ) ;
1021+
1022+ assert_eq ! ( resolved. version, Version :: new( [ 1 , 2 , 3 ] ) ) ;
1023+ assert_eq ! ( mirror_requests. load( Ordering :: SeqCst ) , 1 ) ;
1024+ assert_eq ! ( canonical_requests. load( Ordering :: SeqCst ) , 1 ) ;
1025+ }
1026+
1027+ #[ tokio:: test]
1028+ async fn test_manifest_no_matching_version_does_not_fallback ( ) {
1029+ let platform = Platform :: from_env ( ) . unwrap ( ) ;
1030+ let platform_name = platform. as_cargo_dist_triple ( ) ;
1031+ let ( mirror_url, mirror_requests, mirror_shutdown, mirror_handle) = spawn_http_server (
1032+ manifest_response ( & uv_manifest_line ( "1.2.3" , & platform_name) ) ,
1033+ ) ;
1034+ let ( canonical_url, canonical_requests, canonical_shutdown, canonical_handle) =
1035+ spawn_http_server ( manifest_response ( & uv_manifest_line (
1036+ "9.9.9" ,
1037+ & platform_name,
1038+ ) ) ) ;
1039+ let constraints =
1040+ VersionSpecifiers :: from ( VersionSpecifier :: equals_version ( Version :: new ( [ 9 , 9 , 9 ] ) ) ) ;
1041+
1042+ let err =
1043+ resolve_version_from_manifest_urls ( & [ mirror_url, canonical_url] , Some ( & constraints) )
1044+ . await
1045+ . expect_err ( "no matching version should not fall back to canonical manifest" ) ;
1046+
1047+ let _ = mirror_shutdown. send ( ( ) ) ;
1048+ let _ = canonical_shutdown. send ( ( ) ) ;
1049+ mirror_handle. join ( ) . unwrap ( ) ;
1050+ canonical_handle. join ( ) . unwrap ( ) ;
1051+
1052+ assert ! ( matches!( err, Error :: NoMatchingVersion { .. } ) ) ;
1053+ assert_eq ! ( mirror_requests. load( Ordering :: SeqCst ) , 1 ) ;
1054+ assert_eq ! ( canonical_requests. load( Ordering :: SeqCst ) , 0 ) ;
1055+ }
1056+
8891057 /// Verify that `should_try_next_url` returns `true` even for streaming errors
8901058 /// that `retryable_on_request_failure` does not recognise as transient.
8911059 ///
0 commit comments