@@ -220,7 +220,7 @@ fn get_search_results(
220
220
}
221
221
} ) ;
222
222
223
- let releases: CratesIoSearchResult = HTTP_CLIENT . get ( url) . send ( ) ?. json ( ) ?;
223
+ let releases: CratesIoSearchResult = HTTP_CLIENT . get ( url) . send ( ) ?. error_for_status ( ) ? . json ( ) ?;
224
224
225
225
let names: Vec < _ > = releases
226
226
. crates
@@ -594,18 +594,30 @@ pub fn search_handler(req: &mut Request) -> IronResult<Response> {
594
594
let search_result = ctry ! (
595
595
req,
596
596
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)
609
621
} else if !query. is_empty( ) {
610
622
let query_params: String = form_urlencoded:: Serializer :: new( String :: new( ) )
611
623
. append_pair( "q" , & query)
@@ -748,8 +760,10 @@ mod tests {
748
760
use chrono:: { Duration , TimeZone } ;
749
761
use kuchiki:: traits:: TendrilSink ;
750
762
use mockito:: { mock, Matcher } ;
763
+ use reqwest:: StatusCode ;
751
764
use serde_json:: json;
752
765
use std:: collections:: HashSet ;
766
+ use test_case:: test_case;
753
767
754
768
#[ test]
755
769
fn get_releases_by_stars ( ) {
@@ -911,6 +925,45 @@ mod tests {
911
925
} )
912
926
}
913
927
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
+
914
967
#[ test]
915
968
fn search_encoded_pagination_passed_to_cratesio ( ) {
916
969
wrapper ( |env| {
0 commit comments