cardinal_base/destinations/
container.rs

1use crate::context::CardinalContext;
2use crate::destinations::matcher::DestinationMatcherIndex;
3use crate::provider::Provider;
4use crate::router::CardinalRouter;
5use async_trait::async_trait;
6use cardinal_config::{Destination, Middleware, MiddlewareType};
7use cardinal_errors::CardinalError;
8use pingora::http::RequestHeader;
9use std::collections::BTreeMap;
10use std::sync::Arc;
11
12pub struct DestinationWrapper {
13    pub destination: Destination,
14    pub router: CardinalRouter,
15    pub has_routes: bool,
16    inbound_middleware: Vec<Middleware>,
17    outbound_middleware: Vec<Middleware>,
18}
19
20impl DestinationWrapper {
21    pub fn new(destination: Destination, router: Option<CardinalRouter>) -> Self {
22        let inbound_middleware = destination
23            .middleware
24            .iter()
25            .filter(|&e| e.r#type == MiddlewareType::Inbound)
26            .cloned()
27            .collect();
28        let outbound_middleware = destination
29            .middleware
30            .iter()
31            .filter(|&e| e.r#type == MiddlewareType::Outbound)
32            .cloned()
33            .collect();
34
35        Self {
36            has_routes: !destination.routes.is_empty(),
37            destination,
38            router: router.unwrap_or_default(),
39            inbound_middleware,
40            outbound_middleware,
41        }
42    }
43
44    pub fn get_inbound_middleware(&self) -> &Vec<Middleware> {
45        &self.inbound_middleware
46    }
47
48    pub fn get_outbound_middleware(&self) -> &Vec<Middleware> {
49        &self.outbound_middleware
50    }
51}
52
53pub struct DestinationContainer {
54    destinations: BTreeMap<String, Arc<DestinationWrapper>>,
55    default_destination: Option<Arc<DestinationWrapper>>,
56    matcher: DestinationMatcherIndex,
57}
58
59impl DestinationContainer {
60    pub fn get_backend_for_request(
61        &self,
62        req: &RequestHeader,
63        force_parameter: bool,
64    ) -> Option<Arc<DestinationWrapper>> {
65        let matcher_hit = if force_parameter {
66            None
67        } else {
68            self.matcher.resolve(req)
69        };
70
71        matcher_hit.or_else(|| {
72            let candidate = if force_parameter {
73                first_path_segment(req)
74            } else {
75                extract_subdomain(req)
76            };
77
78            candidate
79                .and_then(|key| self.destinations.get(&key).cloned())
80                .or_else(|| self.default_destination.clone())
81        })
82    }
83}
84
85#[async_trait]
86impl Provider for DestinationContainer {
87    async fn provide(ctx: &CardinalContext) -> Result<Self, CardinalError> {
88        let mut destinations: BTreeMap<String, Arc<DestinationWrapper>> = BTreeMap::new();
89        let mut default_destination = None;
90        let mut wrappers: Vec<Arc<DestinationWrapper>> = Vec::new();
91
92        for (key, destination) in ctx.config.destinations.clone() {
93            let has_match = destination
94                .r#match
95                .as_ref()
96                .map(|entries| !entries.is_empty())
97                .unwrap_or(false);
98            let router = destination
99                .routes
100                .iter()
101                .fold(CardinalRouter::new(), |mut r, route| {
102                    let _ = r.add(route.method.as_str(), route.path.as_str());
103                    r
104                });
105
106            let wrapper = Arc::new(DestinationWrapper::new(destination, Some(router)));
107
108            if wrapper.destination.default {
109                default_destination = Some(wrapper.clone());
110            }
111
112            if !has_match {
113                destinations.insert(key, Arc::clone(&wrapper));
114            }
115            // Every destination participates in the matcher, even if it also lives in the
116            // legacy map (for matcher-less configs).
117            wrappers.push(wrapper);
118        }
119
120        let matcher = DestinationMatcherIndex::new(wrappers.into_iter())?;
121
122        Ok(Self {
123            destinations,
124            default_destination,
125            matcher,
126        })
127    }
128}
129
130fn first_path_segment(req: &RequestHeader) -> Option<String> {
131    let path = req.uri.path();
132    path.strip_prefix('/')
133        .and_then(|p| p.split('/').next())
134        .filter(|s| !s.is_empty())
135        .map(|s| s.to_ascii_lowercase())
136}
137
138fn extract_subdomain(req: &RequestHeader) -> Option<String> {
139    let host = req.uri.host().map(|h| h.to_string()).or_else(|| {
140        req.headers
141            .get("host")
142            .and_then(|v| v.to_str().ok())
143            .map(|s| s.to_string())
144    })?;
145
146    let host_no_port = host.split(':').next()?.to_ascii_lowercase();
147
148    // Only treat as valid when there is a true subdomain: at least sub.domain.tld
149    let parts: Vec<&str> = host_no_port.split('.').collect();
150    if parts.len() < 3 {
151        return None;
152    }
153
154    let first = parts[0];
155    if first.is_empty() || first == "www" {
156        None
157    } else {
158        Some(first.to_string())
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165    use cardinal_config::{Destination, DestinationMatch, DestinationMatchValue};
166    use http::{Method, Uri};
167    use std::collections::BTreeMap;
168    use std::sync::Arc;
169
170    fn req_with_path(pq: &str) -> RequestHeader {
171        RequestHeader::build(Method::GET, pq.as_bytes(), None).unwrap()
172    }
173
174    #[test]
175    fn first_segment_basic() {
176        let req = req_with_path("/api/users");
177        assert_eq!(first_path_segment(&req), Some("api".to_string()));
178    }
179
180    #[test]
181    fn first_segment_root_none() {
182        let req = req_with_path("/");
183        assert_eq!(first_path_segment(&req), None);
184    }
185
186    #[test]
187    fn first_segment_case_insensitive() {
188        let req = req_with_path("/API/v1");
189        assert_eq!(first_path_segment(&req), Some("api".to_string()));
190    }
191
192    #[test]
193    fn first_segment_trailing_slash() {
194        let req = req_with_path("/api/");
195        assert_eq!(first_path_segment(&req), Some("api".to_string()));
196    }
197
198    fn req_with_host_header(host: &str, path: &str) -> RequestHeader {
199        let mut req = req_with_path(path);
200        req.insert_header("host", host).unwrap();
201        req
202    }
203
204    #[test]
205    fn subdomain_from_host_header_basic() {
206        let req = req_with_host_header("api.mygateway.com", "/any");
207        assert_eq!(extract_subdomain(&req), Some("api".to_string()));
208    }
209
210    #[test]
211    fn subdomain_from_host_header_with_port() {
212        let req = req_with_host_header("api.mygateway.com:8080", "/any");
213        assert_eq!(extract_subdomain(&req), Some("api".to_string()));
214    }
215
216    #[test]
217    fn subdomain_www_is_ignored() {
218        let req = req_with_host_header("www.mygateway.com", "/any");
219        assert_eq!(extract_subdomain(&req), None);
220    }
221
222    #[test]
223    fn subdomain_requires_at_least_domain_and_tld() {
224        let req = req_with_host_header("localhost", "/any");
225        assert_eq!(extract_subdomain(&req), None);
226    }
227
228    #[test]
229    fn apex_domain_returns_none() {
230        let req = req_with_host_header("mygateway.com", "/any");
231        assert_eq!(extract_subdomain(&req), None);
232    }
233
234    #[test]
235    fn subdomain_from_uri_authority() {
236        let mut req = req_with_path("/any");
237        let uri: Uri = "http://API.Example.com/any".parse().unwrap();
238        req.set_uri(uri);
239        assert_eq!(extract_subdomain(&req), Some("api".to_string()));
240    }
241
242    fn destination_config(
243        name: &str,
244        host: Option<DestinationMatchValue>,
245        path_prefix: Option<DestinationMatchValue>,
246        path_exact: Option<&str>,
247        default: bool,
248    ) -> Destination {
249        Destination {
250            name: name.to_string(),
251            url: format!("https://{name}.internal"),
252            health_check: None,
253            default,
254            r#match: Some(vec![DestinationMatch {
255                host,
256                path_prefix,
257                path_exact: path_exact.map(|s| s.to_string()),
258            }]),
259            routes: Vec::new(),
260            middleware: Vec::new(),
261            timeout: None,
262            retry: None,
263        }
264    }
265
266    fn build_container(entries: Vec<(&str, Destination)>) -> DestinationContainer {
267        let mut destinations = BTreeMap::new();
268        let mut default_destination = None;
269        let mut wrappers = Vec::new();
270
271        for (key, destination) in entries {
272            let has_match = destination
273                .r#match
274                .as_ref()
275                .map(|entries| !entries.is_empty())
276                .unwrap_or(false);
277            let wrapper = Arc::new(DestinationWrapper::new(destination, None));
278            if wrapper.destination.default {
279                default_destination = Some(wrapper.clone());
280            }
281            if !has_match {
282                destinations.insert(key.to_string(), Arc::clone(&wrapper));
283            }
284            // The matcher should see every destination regardless of legacy eligibility.
285            wrappers.push(wrapper);
286        }
287
288        let matcher = DestinationMatcherIndex::new(wrappers.into_iter()).unwrap();
289
290        DestinationContainer {
291            destinations,
292            default_destination,
293            matcher,
294        }
295    }
296
297    #[test]
298    fn resolves_destination_by_host_match() {
299        let container = build_container(vec![(
300            "customer",
301            destination_config(
302                "customer",
303                Some(DestinationMatchValue::String("support.example.com".into())),
304                None,
305                None,
306                false,
307            ),
308        )]);
309
310        let req = req_with_host_header("support.example.com", "/any");
311        let resolved = container.get_backend_for_request(&req, false).unwrap();
312        assert_eq!(resolved.destination.name, "customer");
313    }
314
315    #[test]
316    fn resolves_destination_by_host_regex() {
317        let container = build_container(vec![(
318            "billing",
319            destination_config(
320                "billing",
321                Some(DestinationMatchValue::Regex {
322                    regex: "^api\\.(eu|us)\\.example\\.com$".into(),
323                }),
324                None,
325                None,
326                false,
327            ),
328        )]);
329
330        let req = req_with_host_header("api.eu.example.com", "/billing/pay");
331        let resolved = container.get_backend_for_request(&req, false).unwrap();
332        assert_eq!(resolved.destination.name, "billing");
333    }
334
335    #[test]
336    fn resolves_destination_by_path_prefix() {
337        let container = build_container(vec![(
338            "helpdesk",
339            destination_config(
340                "helpdesk",
341                None,
342                Some(DestinationMatchValue::String("/helpdesk".into())),
343                None,
344                false,
345            ),
346        )]);
347
348        let req = req_with_host_header("any.example.com", "/helpdesk/ticket");
349        let resolved = container.get_backend_for_request(&req, false).unwrap();
350        assert_eq!(resolved.destination.name, "helpdesk");
351    }
352
353    #[test]
354    fn falls_back_to_default_when_no_match() {
355        let container = build_container(vec![(
356            "primary",
357            destination_config(
358                "primary",
359                Some(DestinationMatchValue::String("app.example.com".into())),
360                None,
361                None,
362                true,
363            ),
364        )]);
365
366        let req = req_with_host_header("unknown.example.com", "/unknown");
367        let resolved = container.get_backend_for_request(&req, false).unwrap();
368        assert_eq!(resolved.destination.name, "primary");
369    }
370
371    #[test]
372    fn does_not_reintroduce_non_matching_host_rule() {
373        let mut entries = Vec::new();
374        entries.push((
375            "billing",
376            destination_config(
377                "billing",
378                Some(DestinationMatchValue::String("billing.example.com".into())),
379                Some(DestinationMatchValue::String("/billing".into())),
380                None,
381                false,
382            ),
383        ));
384
385        let default_destination = Destination {
386            name: "fallback".into(),
387            url: "https://fallback.internal".into(),
388            health_check: None,
389            default: true,
390            r#match: None,
391            routes: Vec::new(),
392            middleware: Vec::new(),
393            timeout: None,
394            retry: None,
395        };
396
397        entries.push(("fallback", default_destination));
398
399        let container = build_container(entries);
400        let req = req_with_host_header("billing.example.com", "/other");
401
402        let resolved = container.get_backend_for_request(&req, false).unwrap();
403        assert_eq!(resolved.destination.name, "fallback");
404    }
405
406    #[test]
407    fn selects_destination_among_shared_host_paths() {
408        let container = build_container(vec![
409            (
410                "billing",
411                destination_config(
412                    "billing",
413                    Some(DestinationMatchValue::String("api.example.com".into())),
414                    Some(DestinationMatchValue::String("/billing".into())),
415                    None,
416                    false,
417                ),
418            ),
419            (
420                "support",
421                destination_config(
422                    "support",
423                    Some(DestinationMatchValue::String("api.example.com".into())),
424                    Some(DestinationMatchValue::String("/support".into())),
425                    None,
426                    false,
427                ),
428            ),
429            (
430                "fallback",
431                Destination {
432                    name: "fallback".into(),
433                    url: "https://fallback.internal".into(),
434                    health_check: None,
435                    default: true,
436                    r#match: None,
437                    routes: Vec::new(),
438                    middleware: Vec::new(),
439                    timeout: None,
440                    retry: None,
441                },
442            ),
443        ]);
444
445        let req = req_with_host_header("api.example.com", "/support/ticket");
446        let resolved = container.get_backend_for_request(&req, false).unwrap();
447        assert_eq!(resolved.destination.name, "support");
448    }
449
450    #[test]
451    fn falls_back_when_shared_host_paths_do_not_match() {
452        let container = build_container(vec![
453            (
454                "billing",
455                destination_config(
456                    "billing",
457                    Some(DestinationMatchValue::String("api.example.com".into())),
458                    Some(DestinationMatchValue::String("/billing".into())),
459                    None,
460                    false,
461                ),
462            ),
463            (
464                "fallback",
465                Destination {
466                    name: "fallback".into(),
467                    url: "https://fallback.internal".into(),
468                    health_check: None,
469                    default: true,
470                    r#match: None,
471                    routes: Vec::new(),
472                    middleware: Vec::new(),
473                    timeout: None,
474                    retry: None,
475                },
476            ),
477        ]);
478
479        let req = req_with_host_header("api.example.com", "/reports");
480        let resolved = container.get_backend_for_request(&req, false).unwrap();
481        assert_eq!(resolved.destination.name, "fallback");
482    }
483
484    #[test]
485    fn host_regex_entries_consider_path_rules() {
486        let container = build_container(vec![
487            (
488                "billing",
489                destination_config(
490                    "billing",
491                    Some(DestinationMatchValue::Regex {
492                        regex: "^api\\.(eu|us)\\.example\\.com$".into(),
493                    }),
494                    Some(DestinationMatchValue::String("/billing".into())),
495                    None,
496                    false,
497                ),
498            ),
499            (
500                "support",
501                destination_config(
502                    "support",
503                    Some(DestinationMatchValue::Regex {
504                        regex: "^api\\.(eu|us)\\.example\\.com$".into(),
505                    }),
506                    Some(DestinationMatchValue::String("/support".into())),
507                    None,
508                    false,
509                ),
510            ),
511            (
512                "fallback",
513                Destination {
514                    name: "fallback".into(),
515                    url: "https://fallback.internal".into(),
516                    health_check: None,
517                    default: true,
518                    r#match: None,
519                    routes: Vec::new(),
520                    middleware: Vec::new(),
521                    timeout: None,
522                    retry: None,
523                },
524            ),
525        ]);
526
527        let req = req_with_host_header("api.eu.example.com", "/support/chat");
528        let resolved = container.get_backend_for_request(&req, false).unwrap();
529        assert_eq!(resolved.destination.name, "support");
530    }
531
532    #[test]
533    fn hostless_entries_respect_path_order() {
534        let container = build_container(vec![
535            (
536                "reports",
537                destination_config(
538                    "reports",
539                    None,
540                    Some(DestinationMatchValue::Regex {
541                        regex: "^/reports/(daily|weekly)".into(),
542                    }),
543                    None,
544                    false,
545                ),
546            ),
547            (
548                "billing",
549                destination_config(
550                    "billing",
551                    None,
552                    Some(DestinationMatchValue::String("/billing".into())),
553                    None,
554                    false,
555                ),
556            ),
557            (
558                "fallback",
559                Destination {
560                    name: "fallback".into(),
561                    url: "https://fallback.internal".into(),
562                    health_check: None,
563                    default: true,
564                    r#match: None,
565                    routes: Vec::new(),
566                    middleware: Vec::new(),
567                    timeout: None,
568                    retry: None,
569                },
570            ),
571        ]);
572
573        let req_reports = req_with_host_header("any.example.com", "/reports/daily/summary");
574        let resolved_reports = container
575            .get_backend_for_request(&req_reports, false)
576            .unwrap();
577        assert_eq!(resolved_reports.destination.name, "reports");
578
579        let req_billing = req_with_host_header("any.example.com", "/billing/invoice");
580        let resolved_billing = container
581            .get_backend_for_request(&req_billing, false)
582            .unwrap();
583        assert_eq!(resolved_billing.destination.name, "billing");
584
585        let req_fallback = req_with_host_header("any.example.com", "/unknown");
586        let resolved_fallback = container
587            .get_backend_for_request(&req_fallback, false)
588            .unwrap();
589        assert_eq!(resolved_fallback.destination.name, "fallback");
590    }
591
592    #[test]
593    fn force_parameter_ignores_match_enabled_destinations() {
594        let container = build_container(vec![
595            (
596                "matched",
597                destination_config(
598                    "matched",
599                    Some(DestinationMatchValue::String("api.example.com".into())),
600                    Some(DestinationMatchValue::String("/matched".into())),
601                    None,
602                    false,
603                ),
604            ),
605            (
606                "fallback",
607                Destination {
608                    name: "fallback".into(),
609                    url: "https://fallback.internal".into(),
610                    health_check: None,
611                    default: true,
612                    r#match: None,
613                    routes: Vec::new(),
614                    middleware: Vec::new(),
615                    timeout: None,
616                    retry: None,
617                },
618            ),
619        ]);
620
621        let req = req_with_path("/matched/orders");
622        let resolved = container.get_backend_for_request(&req, true).unwrap();
623        assert_eq!(resolved.destination.name, "fallback");
624    }
625
626    #[test]
627    fn path_exact_precedence_over_prefix() {
628        let container = build_container(vec![
629            (
630                "status_exact",
631                destination_config(
632                    "status_exact",
633                    Some(DestinationMatchValue::String("status.example.com".into())),
634                    None,
635                    Some("/status"),
636                    false,
637                ),
638            ),
639            (
640                "status_prefix",
641                destination_config(
642                    "status_prefix",
643                    Some(DestinationMatchValue::String("status.example.com".into())),
644                    Some(DestinationMatchValue::String("/status".into())),
645                    None,
646                    false,
647                ),
648            ),
649        ]);
650
651        let req = req_with_host_header("status.example.com", "/status");
652        let resolved = container.get_backend_for_request(&req, false).unwrap();
653        assert_eq!(resolved.destination.name, "status_exact");
654    }
655
656    #[test]
657    fn regex_host_prefers_matching_path_before_default() {
658        let container = build_container(vec![
659            (
660                "v1",
661                destination_config(
662                    "v1",
663                    Some(DestinationMatchValue::Regex {
664                        regex: "^api\\.(eu|us)\\.example\\.com$".into(),
665                    }),
666                    Some(DestinationMatchValue::String("/v1".into())),
667                    None,
668                    false,
669                ),
670            ),
671            (
672                "v2",
673                destination_config(
674                    "v2",
675                    Some(DestinationMatchValue::Regex {
676                        regex: "^api\\.(eu|us)\\.example\\.com$".into(),
677                    }),
678                    Some(DestinationMatchValue::String("/v2".into())),
679                    None,
680                    false,
681                ),
682            ),
683            (
684                "fallback",
685                Destination {
686                    name: "fallback".into(),
687                    url: "https://fallback.internal".into(),
688                    health_check: None,
689                    default: true,
690                    r#match: None,
691                    routes: Vec::new(),
692                    middleware: Vec::new(),
693                    timeout: None,
694                    retry: None,
695                },
696            ),
697        ]);
698
699        let req_v2 = req_with_host_header("api.eu.example.com", "/v2/items");
700        let resolved_v2 = container.get_backend_for_request(&req_v2, false).unwrap();
701        assert_eq!(resolved_v2.destination.name, "v2");
702
703        let req_none = req_with_host_header("api.eu.example.com", "/v3/unknown");
704        let resolved_none = container.get_backend_for_request(&req_none, false).unwrap();
705        assert_eq!(resolved_none.destination.name, "fallback");
706    }
707
708    #[test]
709    fn hostless_entries_prioritize_config_order() {
710        let container = build_container(vec![
711            (
712                "reports_regex",
713                destination_config(
714                    "reports_regex",
715                    None,
716                    Some(DestinationMatchValue::Regex {
717                        regex: "^/reports/.*".into(),
718                    }),
719                    None,
720                    false,
721                ),
722            ),
723            (
724                "reports_prefix",
725                destination_config(
726                    "reports_prefix",
727                    None,
728                    Some(DestinationMatchValue::String("/reports".into())),
729                    None,
730                    false,
731                ),
732            ),
733        ]);
734
735        let req = req_with_host_header("any.example.com", "/reports/daily");
736        let resolved = container.get_backend_for_request(&req, false).unwrap();
737        assert_eq!(resolved.destination.name, "reports_regex");
738    }
739
740    #[test]
741    fn force_parameter_falls_back_to_default_when_unknown() {
742        let container = build_container(vec![(
743            "fallback",
744            Destination {
745                name: "fallback".into(),
746                url: "https://fallback.internal".into(),
747                health_check: None,
748                default: true,
749                r#match: None,
750                routes: Vec::new(),
751                middleware: Vec::new(),
752                timeout: None,
753                retry: None,
754            },
755        )]);
756
757        let req = req_with_path("/unknown/path");
758        let resolved = container.get_backend_for_request(&req, true).unwrap();
759        assert_eq!(resolved.destination.name, "fallback");
760    }
761
762    #[test]
763    fn returns_none_when_no_match_and_no_default() {
764        let container = build_container(vec![(
765            "matcher_only",
766            destination_config(
767                "matcher_only",
768                Some(DestinationMatchValue::String("api.example.com".into())),
769                Some(DestinationMatchValue::String("/matcher".into())),
770                None,
771                false,
772            ),
773        )]);
774
775        let req = req_with_host_header("api.example.com", "/unknown");
776        let resolved = container.get_backend_for_request(&req, false);
777        assert!(resolved.is_none());
778    }
779
780    #[test]
781    fn multi_match_destination_skips_legacy_map() {
782        let destination = Destination {
783            name: "shared".into(),
784            url: "https://shared.internal".into(),
785            health_check: None,
786            default: false,
787            r#match: Some(vec![
788                DestinationMatch {
789                    host: Some(DestinationMatchValue::String("api.example.com".into())),
790                    path_prefix: Some(DestinationMatchValue::String("/billing".into())),
791                    path_exact: None,
792                },
793                DestinationMatch {
794                    host: Some(DestinationMatchValue::Regex {
795                        regex: "^api\\..+".into(),
796                    }),
797                    path_prefix: Some(DestinationMatchValue::String("/regex".into())),
798                    path_exact: None,
799                },
800            ]),
801            routes: Vec::new(),
802            middleware: Vec::new(),
803            timeout: None,
804            retry: None,
805        };
806
807        let container = build_container(vec![("shared", destination)]);
808
809        assert!(container.destinations.get("shared").is_none());
810
811        let exact_req = req_with_host_header("api.example.com", "/billing/invoices");
812        let exact_resolved = container
813            .get_backend_for_request(&exact_req, false)
814            .unwrap();
815        assert_eq!(exact_resolved.destination.name, "shared");
816
817        let regex_req = req_with_host_header("api.example.com", "/regex/search");
818        let regex_resolved = container
819            .get_backend_for_request(&regex_req, false)
820            .unwrap();
821        assert_eq!(regex_resolved.destination.name, "shared");
822    }
823
824    #[test]
825    fn force_parameter_uses_path_segment_lookup() {
826        let destination = Destination {
827            name: "segment".into(),
828            url: "https://segment.internal".into(),
829            health_check: None,
830            default: false,
831            r#match: None,
832            routes: Vec::new(),
833            middleware: Vec::new(),
834            timeout: None,
835            retry: None,
836        };
837
838        let container = build_container(vec![("segment", destination)]);
839        let req = req_with_path("/segment/orders");
840
841        let resolved = container.get_backend_for_request(&req, true).unwrap();
842        assert_eq!(resolved.destination.name, "segment");
843    }
844
845    #[test]
846    fn falls_back_to_subdomain_key_when_present() {
847        let destination = Destination {
848            name: "api".into(),
849            url: "https://api.internal".into(),
850            health_check: None,
851            default: false,
852            r#match: None,
853            routes: Vec::new(),
854            middleware: Vec::new(),
855            timeout: None,
856            retry: None,
857        };
858
859        let container = build_container(vec![("api", destination)]);
860        let req = req_with_host_header("api.mygateway.com", "/any");
861
862        let resolved = container.get_backend_for_request(&req, false).unwrap();
863        assert_eq!(resolved.destination.name, "api");
864    }
865}