@@ -220,7 +220,7 @@ fn get_search_results(
220220 }
221221 } ) ;
222222
223- let releases: CratesIoSearchResult = HTTP_CLIENT . get ( url) . send ( ) ?. json ( ) ?;
223+ let releases: CratesIoSearchResult = HTTP_CLIENT . get ( url) . send ( ) ?. error_for_status ( ) ? . json ( ) ?;
224224
225225 let names: Vec < _ > = releases
226226 . crates
@@ -594,18 +594,30 @@ pub fn search_handler(req: &mut Request) -> IronResult<Response> {
594594 let search_result = ctry ! (
595595 req,
596596 if let Some ( paginate) = params. get( "paginate" ) {
597- get_search_results(
598- & mut conn,
599- & String :: from_utf8_lossy( & base64:: decode( & paginate. as_bytes( ) ) . map_err(
600- |e| -> IronError {
601- warn!(
602- "error when decoding pagination base64 string \" {}\" : {:?}" ,
603- paginate, e
604- ) ;
605- Nope :: NoResults . into( )
606- } ,
607- ) ?) ,
608- )
597+ let decoded = base64:: decode( & paginate. as_bytes( ) ) . map_err( |e| -> IronError {
598+ warn!(
599+ "error when decoding pagination base64 string \" {}\" : {:?}" ,
600+ paginate, e
601+ ) ;
602+ Nope :: NoResults . into( )
603+ } ) ?;
604+ let query_params = String :: from_utf8_lossy( & decoded) ;
605+
606+ if !query_params. starts_with( '?' ) {
607+ // sometimes we see plain bytes being passed to `paginate`.
608+ // In these cases we just return `NoResults` and don't call
609+ // the crates.io API.
610+ // The whole point of the `paginate` design is that we don't
611+ // know anything about the pagination args and crates.io can
612+ // change them as they whish, so we cannot do any more checks here.
613+ warn!(
614+ "didn't get query args in `paginate` arguments for search: \" {}\" " ,
615+ query_params
616+ ) ;
617+ return Err ( Nope :: NoResults . into( ) ) ;
618+ }
619+
620+ get_search_results( & mut conn, & query_params)
609621 } else if !query. is_empty( ) {
610622 let query_params: String = form_urlencoded:: Serializer :: new( String :: new( ) )
611623 . append_pair( "q" , & query)
@@ -748,8 +760,10 @@ mod tests {
748760 use chrono:: { Duration , TimeZone } ;
749761 use kuchiki:: traits:: TendrilSink ;
750762 use mockito:: { mock, Matcher } ;
763+ use reqwest:: StatusCode ;
751764 use serde_json:: json;
752765 use std:: collections:: HashSet ;
766+ use test_case:: test_case;
753767
754768 #[ test]
755769 fn get_releases_by_stars ( ) {
@@ -911,6 +925,45 @@ mod tests {
911925 } )
912926 }
913927
928+ #[ test]
929+ fn search_invalid_paginate_doesnt_request_cratesio ( ) {
930+ wrapper ( |env| {
931+ let response = env
932+ . frontend ( )
933+ . get ( & format ! (
934+ "/releases/search?paginate={}" ,
935+ base64:: encode( "something_that_doesnt_start_with_?" )
936+ ) )
937+ . send ( ) ?;
938+ assert_eq ! ( response. status( ) , StatusCode :: NOT_FOUND ) ;
939+ Ok ( ( ) )
940+ } )
941+ }
942+
943+ #[ test_case( StatusCode :: NOT_FOUND ) ]
944+ #[ test_case( StatusCode :: INTERNAL_SERVER_ERROR ) ]
945+ #[ test_case( StatusCode :: BAD_GATEWAY ) ]
946+ fn crates_io_errors_are_correctly_returned_and_we_dont_try_parsing ( status : StatusCode ) {
947+ wrapper ( |env| {
948+ let _m = mock ( "GET" , "/api/v1/crates" )
949+ . match_query ( Matcher :: AllOf ( vec ! [
950+ Matcher :: UrlEncoded ( "q" . into( ) , "doesnt_matter_here" . into( ) ) ,
951+ Matcher :: UrlEncoded ( "per_page" . into( ) , "30" . into( ) ) ,
952+ ] ) )
953+ . with_status ( status. as_u16 ( ) as usize )
954+ . create ( ) ;
955+
956+ let response = env
957+ . frontend ( )
958+ . get ( "/releases/search?query=doesnt_matter_here" )
959+ . send ( ) ?;
960+ assert_eq ! ( response. status( ) , 500 ) ;
961+
962+ assert ! ( response. text( ) ?. contains( & format!( "{}" , status) ) ) ;
963+ Ok ( ( ) )
964+ } )
965+ }
966+
914967 #[ test]
915968 fn search_encoded_pagination_passed_to_cratesio ( ) {
916969 wrapper ( |env| {
0 commit comments