@@ -44,11 +44,11 @@ pub fn route(method: &Method, path: &str, _raw_query: &str) -> Option<Route> {
4444 return None ;
4545 }
4646 let path = path. trim_start_matches ( '/' ) ;
47- let segs : Vec < & str > = if path . is_empty ( ) {
48- Vec :: new ( )
49- } else {
50- path . split ( '/' ) . collect ( )
51- } ;
47+ // Filter out empty segments so a trailing slash (`/hostedzone/`, which
48+ // botocore/AWS CLI < 1.40 and curl emit for a collection root) routes to the
49+ // List op, not GetHostedZone(id="") -> NoSuchHostedZone (the #1645 shape;
50+ // bug-audit 2026-06-20, 1.1).
51+ let segs : Vec < & str > = path . split ( '/' ) . filter ( |s| !s . is_empty ( ) ) . collect ( ) ;
5252
5353 match ( method, segs. as_slice ( ) ) {
5454 // ─── Hosted Zones ────────────────────────────────────────────
@@ -265,4 +265,19 @@ mod tests {
265265 Some ( Route :: with_id( "ChangeResourceRecordSets" , "Z123" ) )
266266 ) ;
267267 }
268+
269+ #[ test]
270+ fn trailing_slash_collection_routes_to_list ( ) {
271+ // botocore/AWS CLI < 1.40 and curl append `/` to a bare collection URI;
272+ // it must route to the List op, not GetHostedZone(id="") (1.1).
273+ assert_eq ! (
274+ route( & Method :: GET , "/2013-04-01/hostedzone/" , "" ) ,
275+ Some ( Route :: just( "ListHostedZones" ) )
276+ ) ;
277+ // Without a trailing slash still lists.
278+ assert_eq ! (
279+ route( & Method :: GET , "/2013-04-01/hostedzone" , "" ) ,
280+ Some ( Route :: just( "ListHostedZones" ) )
281+ ) ;
282+ }
268283}
0 commit comments