Skip to main content

apollo_router/configuration/
mod.rs

1//! Logic for loading configuration in to an object model
2use std::fmt;
3use std::hash::Hash;
4use std::io;
5use std::io::BufReader;
6use std::iter;
7use std::net::IpAddr;
8use std::net::SocketAddr;
9use std::num::NonZeroU32;
10use std::num::NonZeroUsize;
11use std::str::FromStr;
12use std::sync::Arc;
13use std::time::Duration;
14
15use derivative::Derivative;
16use displaydoc::Display;
17use itertools::Itertools;
18use once_cell::sync::Lazy;
19pub(crate) use persisted_queries::PersistedQueries;
20pub(crate) use persisted_queries::PersistedQueriesPrewarmQueryPlanCache;
21#[cfg(test)]
22pub(crate) use persisted_queries::PersistedQueriesSafelist;
23use regex::Regex;
24use rustls::Certificate;
25use rustls::PrivateKey;
26use rustls::ServerConfig;
27use rustls_pemfile::Item;
28use rustls_pemfile::certs;
29use rustls_pemfile::read_one;
30use schemars::JsonSchema;
31use schemars::gen::SchemaGenerator;
32use schemars::schema::ObjectValidation;
33use schemars::schema::Schema;
34use schemars::schema::SchemaObject;
35use serde::Deserialize;
36use serde::Deserializer;
37use serde::Serialize;
38use serde_json::Map;
39use serde_json::Value;
40use sha2::Digest;
41use thiserror::Error;
42
43use self::cors::Cors;
44use self::expansion::Expansion;
45pub(crate) use self::experimental::Discussed;
46pub(crate) use self::schema::generate_config_schema;
47pub(crate) use self::schema::generate_upgrade;
48use self::server::Server;
49use self::subgraph::SubgraphConfiguration;
50use crate::ApolloRouterError;
51use crate::cache::DEFAULT_CACHE_CAPACITY;
52use crate::configuration::schema::Mode;
53use crate::graphql;
54use crate::notification::Notify;
55use crate::plugin::plugins;
56use crate::plugins::chaos;
57use crate::plugins::chaos::Config;
58use crate::plugins::limits;
59use crate::plugins::subscription::APOLLO_SUBSCRIPTION_PLUGIN;
60use crate::plugins::subscription::APOLLO_SUBSCRIPTION_PLUGIN_NAME;
61use crate::plugins::subscription::SubscriptionConfig;
62use crate::uplink::UplinkConfig;
63
64pub(crate) mod cors;
65pub(crate) mod expansion;
66mod experimental;
67pub(crate) mod metrics;
68mod persisted_queries;
69mod schema;
70pub(crate) mod server;
71pub(crate) mod shared;
72pub(crate) mod subgraph;
73#[cfg(test)]
74mod tests;
75mod upgrade;
76mod yaml;
77
78// TODO: Talk it through with the teams
79static HEARTBEAT_TIMEOUT_DURATION_SECONDS: u64 = 15;
80
81static SUPERGRAPH_ENDPOINT_REGEX: Lazy<Regex> = Lazy::new(|| {
82    Regex::new(r"(?P<first_path>.*/)(?P<sub_path>.+)\*$")
83        .expect("this regex to check the path is valid")
84});
85
86/// Configuration error.
87#[derive(Debug, Error, Display)]
88#[non_exhaustive]
89pub enum ConfigurationError {
90    /// could not expand variable: {key}, {cause}
91    CannotExpandVariable { key: String, cause: String },
92    /// could not expand variable: {key}. Variables must be prefixed with one of '{supported_modes}' followed by '.' e.g. 'env.'
93    UnknownExpansionMode {
94        key: String,
95        supported_modes: String,
96    },
97    /// unknown plugin {0}
98    PluginUnknown(String),
99    /// plugin {plugin} could not be configured: {error}
100    PluginConfiguration { plugin: String, error: String },
101    /// {message}: {error}
102    InvalidConfiguration {
103        message: &'static str,
104        error: String,
105    },
106    /// could not deserialize configuration: {0}
107    DeserializeConfigError(serde_json::Error),
108
109    /// APOLLO_ROUTER_CONFIG_SUPPORTED_MODES must be of the format env,file,... Possible modes are 'env' and 'file'.
110    InvalidExpansionModeConfig,
111
112    /// could not migrate configuration: {error}.
113    MigrationFailure { error: String },
114
115    /// could not load certificate authorities: {error}
116    CertificateAuthorities { error: String },
117}
118
119/// The configuration for the router.
120///
121/// Can be created through `serde::Deserialize` from various formats,
122/// or inline in Rust code with `serde_json::json!` and `serde_json::from_value`.
123#[derive(Clone, Derivative, Serialize, JsonSchema)]
124#[derivative(Debug)]
125// We can't put a global #[serde(default)] here because of the Default implementation using `from_str` which use deserialize
126pub struct Configuration {
127    /// The raw configuration value.
128    #[serde(skip)]
129    pub(crate) validated_yaml: Option<Value>,
130
131    /// Health check configuration
132    #[serde(default)]
133    pub(crate) health_check: HealthCheck,
134
135    /// Sandbox configuration
136    #[serde(default)]
137    pub(crate) sandbox: Sandbox,
138
139    /// Homepage configuration
140    #[serde(default)]
141    pub(crate) homepage: Homepage,
142
143    /// Configuration for the server
144    #[serde(default)]
145    pub(crate) server: Server,
146
147    /// Configuration for the supergraph
148    #[serde(default)]
149    pub(crate) supergraph: Supergraph,
150
151    /// Cross origin request headers.
152    #[serde(default)]
153    pub(crate) cors: Cors,
154
155    #[serde(default)]
156    pub(crate) tls: Tls,
157
158    /// Configures automatic persisted queries
159    #[serde(default)]
160    pub(crate) apq: Apq,
161
162    /// Configures managed persisted queries
163    #[serde(default)]
164    pub persisted_queries: PersistedQueries,
165
166    /// Configuration for operation limits, parser limits, HTTP limits, etc.
167    #[serde(default)]
168    pub(crate) limits: limits::Config,
169
170    /// Configuration for chaos testing, trying to reproduce bugs that require uncommon conditions.
171    /// You probably don’t want this in production!
172    #[serde(default)]
173    pub(crate) experimental_chaos: Config,
174
175    /// Plugin configuration
176    #[serde(default)]
177    pub(crate) plugins: UserPlugins,
178
179    /// Built-in plugin configuration. Built in plugins are pushed to the top level of config.
180    #[serde(default)]
181    #[serde(flatten)]
182    pub(crate) apollo_plugins: ApolloPlugins,
183
184    /// Uplink configuration.
185    #[serde(skip)]
186    pub uplink: Option<UplinkConfig>,
187
188    #[serde(default, skip_serializing, skip_deserializing)]
189    pub(crate) notify: Notify<String, graphql::Response>,
190
191    /// Batching configuration.
192    #[serde(default)]
193    pub(crate) batching: Batching,
194
195    /// Type conditioned fetching configuration.
196    #[serde(default)]
197    pub(crate) experimental_type_conditioned_fetching: bool,
198}
199
200impl PartialEq for Configuration {
201    fn eq(&self, other: &Self) -> bool {
202        self.validated_yaml == other.validated_yaml
203    }
204}
205
206impl<'de> serde::Deserialize<'de> for Configuration {
207    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
208    where
209        D: serde::Deserializer<'de>,
210    {
211        // This intermediate structure will allow us to deserialize a Configuration
212        // yet still exercise the Configuration validation function
213        #[derive(Deserialize, Default)]
214        #[serde(default)]
215        struct AdHocConfiguration {
216            health_check: HealthCheck,
217            sandbox: Sandbox,
218            homepage: Homepage,
219            server: Server,
220            supergraph: Supergraph,
221            cors: Cors,
222            plugins: UserPlugins,
223            #[serde(flatten)]
224            apollo_plugins: ApolloPlugins,
225            tls: Tls,
226            apq: Apq,
227            persisted_queries: PersistedQueries,
228            limits: limits::Config,
229            experimental_chaos: chaos::Config,
230            batching: Batching,
231            experimental_type_conditioned_fetching: bool,
232        }
233        let mut ad_hoc: AdHocConfiguration = serde::Deserialize::deserialize(deserializer)?;
234
235        let notify = Configuration::notify(&ad_hoc.apollo_plugins.plugins)
236            .map_err(|e| serde::de::Error::custom(e.to_string()))?;
237
238        // Allow the limits plugin to use the configuration from the configuration struct.
239        // This means that the limits plugin will get the regular configuration via plugin init.
240        ad_hoc.apollo_plugins.plugins.insert(
241            "limits".to_string(),
242            serde_json::to_value(&ad_hoc.limits).unwrap(),
243        );
244
245        // Use a struct literal instead of a builder to ensure this is exhaustive
246        Configuration {
247            health_check: ad_hoc.health_check,
248            sandbox: ad_hoc.sandbox,
249            homepage: ad_hoc.homepage,
250            server: ad_hoc.server,
251            supergraph: ad_hoc.supergraph,
252            cors: ad_hoc.cors,
253            tls: ad_hoc.tls,
254            apq: ad_hoc.apq,
255            persisted_queries: ad_hoc.persisted_queries,
256            limits: ad_hoc.limits,
257            experimental_chaos: ad_hoc.experimental_chaos,
258            experimental_type_conditioned_fetching: ad_hoc.experimental_type_conditioned_fetching,
259            plugins: ad_hoc.plugins,
260            apollo_plugins: ad_hoc.apollo_plugins,
261            batching: ad_hoc.batching,
262
263            // serde(skip)
264            notify,
265            uplink: None,
266            validated_yaml: None,
267        }
268        .validate()
269        .map_err(|e| serde::de::Error::custom(e.to_string()))
270    }
271}
272
273pub(crate) const APOLLO_PLUGIN_PREFIX: &str = "apollo.";
274
275fn default_graphql_listen() -> ListenAddr {
276    SocketAddr::from_str("127.0.0.1:4000").unwrap().into()
277}
278
279#[cfg(test)]
280fn test_listen() -> ListenAddr {
281    SocketAddr::from_str("127.0.0.1:0").unwrap().into()
282}
283
284#[cfg(test)]
285#[buildstructor::buildstructor]
286impl Configuration {
287    #[builder]
288    pub(crate) fn new(
289        supergraph: Option<Supergraph>,
290        health_check: Option<HealthCheck>,
291        sandbox: Option<Sandbox>,
292        homepage: Option<Homepage>,
293        cors: Option<Cors>,
294        plugins: Map<String, Value>,
295        apollo_plugins: Map<String, Value>,
296        tls: Option<Tls>,
297        apq: Option<Apq>,
298        persisted_query: Option<PersistedQueries>,
299        operation_limits: Option<limits::Config>,
300        chaos: Option<chaos::Config>,
301        uplink: Option<UplinkConfig>,
302        experimental_type_conditioned_fetching: Option<bool>,
303        batching: Option<Batching>,
304        server: Option<Server>,
305    ) -> Result<Self, ConfigurationError> {
306        let notify = Self::notify(&apollo_plugins)?;
307
308        let conf = Self {
309            validated_yaml: Default::default(),
310            supergraph: supergraph.unwrap_or_default(),
311            server: server.unwrap_or_default(),
312            health_check: health_check.unwrap_or_default(),
313            sandbox: sandbox.unwrap_or_default(),
314            homepage: homepage.unwrap_or_default(),
315            cors: cors.unwrap_or_default(),
316            apq: apq.unwrap_or_default(),
317            persisted_queries: persisted_query.unwrap_or_default(),
318            limits: operation_limits.unwrap_or_default(),
319            experimental_chaos: chaos.unwrap_or_default(),
320            plugins: UserPlugins {
321                plugins: Some(plugins),
322            },
323            apollo_plugins: ApolloPlugins {
324                plugins: apollo_plugins,
325            },
326            tls: tls.unwrap_or_default(),
327            uplink,
328            batching: batching.unwrap_or_default(),
329            experimental_type_conditioned_fetching: experimental_type_conditioned_fetching
330                .unwrap_or_default(),
331            notify,
332        };
333
334        conf.validate()
335    }
336}
337
338impl Configuration {
339    pub(crate) fn hash(&self) -> String {
340        let mut hasher = sha2::Sha256::new();
341        let defaulted_raw = self
342            .validated_yaml
343            .as_ref()
344            .map(|s| serde_yaml::to_string(s).expect("config was not serializable"))
345            .unwrap_or_default();
346        hasher.update(defaulted_raw);
347        let hash: String = format!("{:x}", hasher.finalize());
348        hash
349    }
350
351    fn notify(
352        apollo_plugins: &Map<String, Value>,
353    ) -> Result<Notify<String, graphql::Response>, ConfigurationError> {
354        if cfg!(test) {
355            return Ok(Notify::for_tests());
356        }
357        let notify_queue_cap = match apollo_plugins.get(APOLLO_SUBSCRIPTION_PLUGIN_NAME) {
358            Some(plugin_conf) => {
359                let conf = serde_json::from_value::<SubscriptionConfig>(plugin_conf.clone())
360                    .map_err(|err| ConfigurationError::PluginConfiguration {
361                        plugin: APOLLO_SUBSCRIPTION_PLUGIN.to_string(),
362                        error: format!("{err:?}"),
363                    })?;
364                conf.queue_capacity
365            }
366            None => None,
367        };
368        Ok(Notify::builder()
369            .and_queue_size(notify_queue_cap)
370            .ttl(Duration::from_secs(HEARTBEAT_TIMEOUT_DURATION_SECONDS))
371            .heartbeat_error_message(
372                graphql::Response::builder()
373                .errors(vec![
374                    graphql::Error::builder()
375                    .message("the connection has been closed because it hasn't heartbeat for a while")
376                    .extension_code("SUBSCRIPTION_HEARTBEAT_ERROR")
377                    .build()
378                ])
379                .build()
380            ).build())
381    }
382
383    pub(crate) fn rust_query_planner_config(
384        &self,
385    ) -> apollo_federation::query_plan::query_planner::QueryPlannerConfig {
386        use apollo_federation::query_plan::query_planner::QueryPlanIncrementalDeliveryConfig;
387        use apollo_federation::query_plan::query_planner::QueryPlannerConfig;
388        use apollo_federation::query_plan::query_planner::QueryPlannerDebugConfig;
389
390        let max_evaluated_plans = self
391            .supergraph
392            .query_planning
393            .experimental_plans_limit
394            // Fails if experimental_plans_limit is zero; use our default.
395            .and_then(NonZeroU32::new)
396            .unwrap_or(NonZeroU32::new(10_000).expect("it is not zero"));
397
398        QueryPlannerConfig {
399            subgraph_graphql_validation: false,
400            generate_query_fragments: self.supergraph.generate_query_fragments,
401            incremental_delivery: QueryPlanIncrementalDeliveryConfig {
402                enable_defer: self.supergraph.defer_support,
403            },
404            type_conditioned_fetching: self.experimental_type_conditioned_fetching,
405            debug: QueryPlannerDebugConfig {
406                max_evaluated_plans,
407                paths_limit: self.supergraph.query_planning.experimental_paths_limit,
408            },
409        }
410    }
411}
412
413impl Default for Configuration {
414    fn default() -> Self {
415        // We want to trigger all defaulting logic so don't use the raw builder.
416        Configuration::from_str("").expect("default configuration must be valid")
417    }
418}
419
420#[cfg(test)]
421#[buildstructor::buildstructor]
422impl Configuration {
423    #[builder]
424    pub(crate) fn fake_new(
425        supergraph: Option<Supergraph>,
426        health_check: Option<HealthCheck>,
427        sandbox: Option<Sandbox>,
428        homepage: Option<Homepage>,
429        cors: Option<Cors>,
430        plugins: Map<String, Value>,
431        apollo_plugins: Map<String, Value>,
432        tls: Option<Tls>,
433        notify: Option<Notify<String, graphql::Response>>,
434        apq: Option<Apq>,
435        persisted_query: Option<PersistedQueries>,
436        operation_limits: Option<limits::Config>,
437        chaos: Option<chaos::Config>,
438        uplink: Option<UplinkConfig>,
439        batching: Option<Batching>,
440        experimental_type_conditioned_fetching: Option<bool>,
441        server: Option<Server>,
442    ) -> Result<Self, ConfigurationError> {
443        let configuration = Self {
444            validated_yaml: Default::default(),
445            server: server.unwrap_or_default(),
446            supergraph: supergraph.unwrap_or_else(|| Supergraph::fake_builder().build()),
447            health_check: health_check.unwrap_or_else(|| HealthCheck::fake_builder().build()),
448            sandbox: sandbox.unwrap_or_else(|| Sandbox::fake_builder().build()),
449            homepage: homepage.unwrap_or_else(|| Homepage::fake_builder().build()),
450            cors: cors.unwrap_or_default(),
451            limits: operation_limits.unwrap_or_default(),
452            experimental_chaos: chaos.unwrap_or_default(),
453            plugins: UserPlugins {
454                plugins: Some(plugins),
455            },
456            apollo_plugins: ApolloPlugins {
457                plugins: apollo_plugins,
458            },
459            tls: tls.unwrap_or_default(),
460            notify: notify.unwrap_or_default(),
461            apq: apq.unwrap_or_default(),
462            persisted_queries: persisted_query.unwrap_or_default(),
463            uplink,
464            experimental_type_conditioned_fetching: experimental_type_conditioned_fetching
465                .unwrap_or_default(),
466            batching: batching.unwrap_or_default(),
467        };
468
469        configuration.validate()
470    }
471}
472
473impl Configuration {
474    pub(crate) fn validate(self) -> Result<Self, ConfigurationError> {
475        #[cfg(not(feature = "hyper_header_limits"))]
476        if self.limits.http1_max_request_headers.is_some() {
477            return Err(ConfigurationError::InvalidConfiguration {
478                message: "'limits.http1_max_request_headers' requires 'hyper_header_limits' feature",
479                error: "enable 'hyper_header_limits' feature in order to use 'limits.http1_max_request_headers'".to_string(),
480            });
481        }
482
483        // Sandbox and Homepage cannot be both enabled
484        if self.sandbox.enabled && self.homepage.enabled {
485            return Err(ConfigurationError::InvalidConfiguration {
486                message: "sandbox and homepage cannot be enabled at the same time",
487                error: "disable the homepage if you want to enable sandbox".to_string(),
488            });
489        }
490        // Sandbox needs Introspection to be enabled
491        if self.sandbox.enabled && !self.supergraph.introspection {
492            return Err(ConfigurationError::InvalidConfiguration {
493                message: "sandbox requires introspection",
494                error: "sandbox needs introspection to be enabled".to_string(),
495            });
496        }
497        if !self.supergraph.path.starts_with('/') {
498            return Err(ConfigurationError::InvalidConfiguration {
499                message: "invalid 'server.graphql_path' configuration",
500                error: format!(
501                    "'{}' is invalid, it must be an absolute path and start with '/', you should try with '/{}'",
502                    self.supergraph.path, self.supergraph.path
503                ),
504            });
505        }
506        if self.supergraph.path.ends_with('*')
507            && !self.supergraph.path.ends_with("/*")
508            && !SUPERGRAPH_ENDPOINT_REGEX.is_match(&self.supergraph.path)
509        {
510            return Err(ConfigurationError::InvalidConfiguration {
511                message: "invalid 'server.graphql_path' configuration",
512                error: format!(
513                    "'{}' is invalid, you can only set a wildcard after a '/'",
514                    self.supergraph.path
515                ),
516            });
517        }
518        if self.supergraph.path.contains("/*/") {
519            return Err(ConfigurationError::InvalidConfiguration {
520                message: "invalid 'server.graphql_path' configuration",
521                error: format!(
522                    "'{}' is invalid, if you need to set a path like '/*/graphql' then specify it as a path parameter with a name, for example '/:my_project_key/graphql'",
523                    self.supergraph.path
524                ),
525            });
526        }
527
528        // PQs.
529        if self.persisted_queries.enabled {
530            if self.persisted_queries.safelist.enabled && self.apq.enabled {
531                return Err(ConfigurationError::InvalidConfiguration {
532                    message: "apqs must be disabled to enable safelisting",
533                    error: "either set persisted_queries.safelist.enabled: false or apq.enabled: false in your router yaml configuration".into()
534                });
535            } else if !self.persisted_queries.safelist.enabled
536                && self.persisted_queries.safelist.require_id
537            {
538                return Err(ConfigurationError::InvalidConfiguration {
539                    message: "safelist must be enabled to require IDs",
540                    error: "either set persisted_queries.safelist.enabled: true or persisted_queries.safelist.require_id: false in your router yaml configuration".into()
541                });
542            }
543        } else {
544            // If the feature isn't enabled, sub-features shouldn't be.
545            if self.persisted_queries.safelist.enabled {
546                return Err(ConfigurationError::InvalidConfiguration {
547                    message: "persisted queries must be enabled to enable safelisting",
548                    error: "either set persisted_queries.safelist.enabled: false or persisted_queries.enabled: true in your router yaml configuration".into()
549                });
550            } else if self.persisted_queries.log_unknown {
551                return Err(ConfigurationError::InvalidConfiguration {
552                    message: "persisted queries must be enabled to enable logging unknown operations",
553                    error: "either set persisted_queries.log_unknown: false or persisted_queries.enabled: true in your router yaml configuration".into()
554                });
555            }
556        }
557
558        Ok(self)
559    }
560}
561
562/// Parse configuration from a string in YAML syntax
563impl FromStr for Configuration {
564    type Err = ConfigurationError;
565
566    fn from_str(s: &str) -> Result<Self, Self::Err> {
567        schema::validate_yaml_configuration(s, Expansion::default()?, Mode::Upgrade)?.validate()
568    }
569}
570
571fn gen_schema(plugins: schemars::Map<String, Schema>) -> Schema {
572    let plugins_object = SchemaObject {
573        object: Some(Box::new(ObjectValidation {
574            properties: plugins,
575            additional_properties: Option::Some(Box::new(Schema::Bool(false))),
576            ..Default::default()
577        })),
578        ..Default::default()
579    };
580
581    Schema::Object(plugins_object)
582}
583
584/// Plugins provided by Apollo.
585///
586/// These plugins are processed prior to user plugins. Also, their configuration
587/// is "hoisted" to the top level of the config rather than being processed
588/// under "plugins" as for user plugins.
589#[derive(Clone, Debug, Default, Deserialize, Serialize)]
590#[serde(transparent)]
591pub(crate) struct ApolloPlugins {
592    pub(crate) plugins: Map<String, Value>,
593}
594
595impl JsonSchema for ApolloPlugins {
596    fn schema_name() -> String {
597        stringify!(Plugins).to_string()
598    }
599
600    fn json_schema(gen: &mut SchemaGenerator) -> Schema {
601        // This is a manual implementation of Plugins schema to allow plugins that have been registered at
602        // compile time to be picked up.
603
604        let plugins = crate::plugin::plugins()
605            .sorted_by_key(|factory| factory.name.clone())
606            .filter(|factory| {
607                factory.name.starts_with(APOLLO_PLUGIN_PREFIX)
608                    && !factory.hidden_from_config_json_schema
609            })
610            .map(|factory| {
611                (
612                    factory.name[APOLLO_PLUGIN_PREFIX.len()..].to_string(),
613                    factory.create_schema(gen),
614                )
615            })
616            .collect::<schemars::Map<String, Schema>>();
617        gen_schema(plugins)
618    }
619}
620
621/// Plugins provided by a user.
622///
623/// These plugins are compiled into a router by and their configuration is performed
624/// under the "plugins" section.
625#[derive(Clone, Debug, Default, Deserialize, Serialize)]
626#[serde(transparent)]
627pub(crate) struct UserPlugins {
628    pub(crate) plugins: Option<Map<String, Value>>,
629}
630
631impl JsonSchema for UserPlugins {
632    fn schema_name() -> String {
633        stringify!(Plugins).to_string()
634    }
635
636    fn json_schema(gen: &mut SchemaGenerator) -> Schema {
637        // This is a manual implementation of Plugins schema to allow plugins that have been registered at
638        // compile time to be picked up.
639
640        let plugins = crate::plugin::plugins()
641            .sorted_by_key(|factory| factory.name.clone())
642            .filter(|factory| !factory.name.starts_with(APOLLO_PLUGIN_PREFIX))
643            .map(|factory| (factory.name.to_string(), factory.create_schema(gen)))
644            .collect::<schemars::Map<String, Schema>>();
645        gen_schema(plugins)
646    }
647}
648
649/// Configuration options pertaining to the supergraph server component.
650#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
651#[serde(deny_unknown_fields)]
652#[serde(default)]
653pub(crate) struct Supergraph {
654    /// The socket address and port to listen on
655    /// Defaults to 127.0.0.1:4000
656    pub(crate) listen: ListenAddr,
657
658    /// The timeout for shutting down connections during a router shutdown or a schema reload.
659    #[serde(deserialize_with = "humantime_serde::deserialize")]
660    #[schemars(with = "String", default = "default_connection_shutdown_timeout")]
661    pub(crate) connection_shutdown_timeout: Duration,
662
663    /// The HTTP path on which GraphQL requests will be served.
664    /// default: "/"
665    pub(crate) path: String,
666
667    /// Enable introspection
668    /// Default: false
669    pub(crate) introspection: bool,
670
671    /// Enable QP generation of fragments for subgraph requests
672    /// Default: true
673    pub(crate) generate_query_fragments: bool,
674
675    /// Set to false to disable defer support
676    pub(crate) defer_support: bool,
677
678    /// Query planning options
679    pub(crate) query_planning: QueryPlanning,
680
681    /// abort request handling when the client drops the connection.
682    /// Default: false.
683    /// When set to true, some parts of the request pipeline like telemetry will not work properly,
684    /// but request handling will stop immediately when the client connection is closed.
685    pub(crate) early_cancel: bool,
686
687    /// Log a message if the client closes the connection before the response is sent.
688    /// Default: false.
689    pub(crate) experimental_log_on_broken_pipe: bool,
690}
691
692const fn default_generate_query_fragments() -> bool {
693    true
694}
695
696#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize, Serialize, JsonSchema)]
697#[serde(rename_all = "snake_case")]
698pub(crate) enum Auto {
699    Auto,
700}
701
702fn default_defer_support() -> bool {
703    true
704}
705
706#[buildstructor::buildstructor]
707impl Supergraph {
708    #[builder]
709    pub(crate) fn new(
710        listen: Option<ListenAddr>,
711        path: Option<String>,
712        connection_shutdown_timeout: Option<Duration>,
713        introspection: Option<bool>,
714        defer_support: Option<bool>,
715        query_planning: Option<QueryPlanning>,
716        generate_query_fragments: Option<bool>,
717        early_cancel: Option<bool>,
718        experimental_log_on_broken_pipe: Option<bool>,
719    ) -> Self {
720        Self {
721            listen: listen.unwrap_or_else(default_graphql_listen),
722            path: path.unwrap_or_else(default_graphql_path),
723            connection_shutdown_timeout: connection_shutdown_timeout
724                .unwrap_or_else(default_connection_shutdown_timeout),
725            introspection: introspection.unwrap_or_else(default_graphql_introspection),
726            defer_support: defer_support.unwrap_or_else(default_defer_support),
727            query_planning: query_planning.unwrap_or_default(),
728            generate_query_fragments: generate_query_fragments
729                .unwrap_or_else(default_generate_query_fragments),
730            early_cancel: early_cancel.unwrap_or_default(),
731            experimental_log_on_broken_pipe: experimental_log_on_broken_pipe.unwrap_or_default(),
732        }
733    }
734}
735
736#[cfg(test)]
737#[buildstructor::buildstructor]
738impl Supergraph {
739    #[builder]
740    pub(crate) fn fake_new(
741        listen: Option<ListenAddr>,
742        path: Option<String>,
743        connection_shutdown_timeout: Option<Duration>,
744        introspection: Option<bool>,
745        defer_support: Option<bool>,
746        query_planning: Option<QueryPlanning>,
747        generate_query_fragments: Option<bool>,
748        early_cancel: Option<bool>,
749        experimental_log_on_broken_pipe: Option<bool>,
750    ) -> Self {
751        Self {
752            listen: listen.unwrap_or_else(test_listen),
753            path: path.unwrap_or_else(default_graphql_path),
754            connection_shutdown_timeout: connection_shutdown_timeout
755                .unwrap_or_else(default_connection_shutdown_timeout),
756            introspection: introspection.unwrap_or_else(default_graphql_introspection),
757            defer_support: defer_support.unwrap_or_else(default_defer_support),
758            query_planning: query_planning.unwrap_or_default(),
759            generate_query_fragments: generate_query_fragments
760                .unwrap_or_else(default_generate_query_fragments),
761            early_cancel: early_cancel.unwrap_or_default(),
762            experimental_log_on_broken_pipe: experimental_log_on_broken_pipe.unwrap_or_default(),
763        }
764    }
765}
766
767impl Default for Supergraph {
768    fn default() -> Self {
769        Self::builder().build()
770    }
771}
772
773impl Supergraph {
774    /// To sanitize the path for axum router
775    pub(crate) fn sanitized_path(&self) -> String {
776        let mut path = self.path.clone();
777        if self.path.ends_with("/*") {
778            // Needed for axum (check the axum docs for more information about wildcards https://docs.rs/axum/latest/axum/struct.Router.html#wildcards)
779            path = format!("{}router_extra_path", self.path);
780        } else if SUPERGRAPH_ENDPOINT_REGEX.is_match(&self.path) {
781            let new_path = SUPERGRAPH_ENDPOINT_REGEX
782                .replace(&self.path, "${first_path}${sub_path}:supergraph_route");
783            path = new_path.to_string();
784        }
785
786        path
787    }
788}
789
790/// Router level (APQ) configuration
791#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, Default)]
792#[serde(deny_unknown_fields)]
793pub(crate) struct Router {
794    #[serde(default)]
795    pub(crate) cache: Cache,
796}
797
798/// Automatic Persisted Queries (APQ) configuration
799#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
800#[serde(deny_unknown_fields, default)]
801pub(crate) struct Apq {
802    /// Activates Automatic Persisted Queries (enabled by default)
803    pub(crate) enabled: bool,
804
805    pub(crate) router: Router,
806
807    pub(crate) subgraph: SubgraphConfiguration<SubgraphApq>,
808}
809
810#[cfg(test)]
811#[buildstructor::buildstructor]
812impl Apq {
813    #[builder]
814    pub(crate) fn fake_new(enabled: Option<bool>) -> Self {
815        Self {
816            enabled: enabled.unwrap_or_else(default_apq),
817            ..Default::default()
818        }
819    }
820}
821
822/// Subgraph level Automatic Persisted Queries (APQ) configuration
823#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema)]
824#[serde(deny_unknown_fields, default)]
825pub(crate) struct SubgraphApq {
826    /// Enable
827    pub(crate) enabled: bool,
828}
829
830fn default_apq() -> bool {
831    true
832}
833
834impl Default for Apq {
835    fn default() -> Self {
836        Self {
837            enabled: default_apq(),
838            router: Default::default(),
839            subgraph: Default::default(),
840        }
841    }
842}
843
844/// Query planning cache configuration
845#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, Default)]
846#[serde(deny_unknown_fields, default)]
847pub(crate) struct QueryPlanning {
848    /// Cache configuration
849    pub(crate) cache: QueryPlanCache,
850    /// Warms up the cache on reloads by running the query plan over
851    /// a list of the most used queries (from the in memory cache)
852    /// Configures the number of queries warmed up. Defaults to 1/3 of
853    /// the in memory cache
854    pub(crate) warmed_up_queries: Option<usize>,
855
856    /// Sets a limit to the number of generated query plans.
857    /// The planning process generates many different query plans as it
858    /// explores the graph, and the list can grow large. By using this
859    /// limit, we prevent that growth and still get a valid query plan,
860    /// but it may not be the optimal one.
861    ///
862    /// The default limit is set to 10000, but it may change in the future
863    pub(crate) experimental_plans_limit: Option<u32>,
864
865    /// Before creating query plans, for each path of fields in the query we compute all the
866    /// possible options to traverse that path via the subgraphs. Multiple options can arise because
867    /// fields in the path can be provided by multiple subgraphs, and abstract types (i.e. unions
868    /// and interfaces) returned by fields sometimes require the query planner to traverse through
869    /// each constituent object type. The number of options generated in this computation can grow
870    /// large if the schema or query are sufficiently complex, and that will increase the time spent
871    /// planning.
872    ///
873    /// This config allows specifying a per-path limit to the number of options considered. If any
874    /// path's options exceeds this limit, query planning will abort and the operation will fail.
875    ///
876    /// The default value is None, which specifies no limit.
877    pub(crate) experimental_paths_limit: Option<u32>,
878
879    /// If cache warm up is configured, this will allow the router to keep a query plan created with
880    /// the old schema, if it determines that the schema update does not affect the corresponding query
881    pub(crate) experimental_reuse_query_plans: bool,
882}
883
884/// Cache configuration
885#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema)]
886#[serde(deny_unknown_fields, default)]
887pub(crate) struct QueryPlanCache {
888    /// Configures the in memory cache (always active)
889    pub(crate) in_memory: InMemoryCache,
890    /// Configures and activates the Redis cache
891    pub(crate) redis: Option<QueryPlanRedisCache>,
892}
893
894#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
895#[serde(deny_unknown_fields)]
896/// Redis cache configuration
897pub(crate) struct QueryPlanRedisCache {
898    /// List of URLs to the Redis cluster
899    pub(crate) urls: Vec<url::Url>,
900
901    /// Redis username if not provided in the URLs. This field takes precedence over the username in the URL
902    pub(crate) username: Option<String>,
903    /// Redis password if not provided in the URLs. This field takes precedence over the password in the URL
904    pub(crate) password: Option<String>,
905
906    #[serde(deserialize_with = "humantime_serde::deserialize", default)]
907    #[schemars(with = "Option<String>", default)]
908    /// Redis request timeout (default: 2ms)
909    pub(crate) timeout: Option<Duration>,
910
911    #[serde(
912        deserialize_with = "humantime_serde::deserialize",
913        default = "default_query_plan_cache_ttl"
914    )]
915    #[schemars(with = "Option<String>", default = "default_query_plan_cache_ttl")]
916    /// TTL for entries
917    pub(crate) ttl: Duration,
918
919    /// namespace used to prefix Redis keys
920    pub(crate) namespace: Option<String>,
921
922    #[serde(default)]
923    /// TLS client configuration
924    pub(crate) tls: Option<TlsClient>,
925
926    #[serde(default = "default_required_to_start")]
927    /// Prevents the router from starting if it cannot connect to Redis
928    pub(crate) required_to_start: bool,
929
930    #[serde(default = "default_reset_ttl")]
931    /// When a TTL is set on a key, reset it when reading the data from that key
932    pub(crate) reset_ttl: bool,
933
934    #[serde(default = "default_query_planner_cache_pool_size")]
935    /// The size of the Redis connection pool
936    pub(crate) pool_size: u32,
937}
938
939fn default_query_plan_cache_ttl() -> Duration {
940    // Default TTL set to 30 days
941    Duration::from_secs(86400 * 30)
942}
943
944fn default_query_planner_cache_pool_size() -> u32 {
945    1
946}
947
948/// Cache configuration
949#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema)]
950#[serde(deny_unknown_fields, default)]
951pub(crate) struct Cache {
952    /// Configures the in memory cache (always active)
953    pub(crate) in_memory: InMemoryCache,
954    /// Configures and activates the Redis cache
955    pub(crate) redis: Option<RedisCache>,
956}
957
958impl From<QueryPlanCache> for Cache {
959    fn from(value: QueryPlanCache) -> Self {
960        Cache {
961            in_memory: value.in_memory,
962            redis: value.redis.map(Into::into),
963        }
964    }
965}
966
967#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
968#[serde(deny_unknown_fields)]
969/// In memory cache configuration
970pub(crate) struct InMemoryCache {
971    /// Number of entries in the Least Recently Used cache
972    pub(crate) limit: NonZeroUsize,
973}
974
975impl Default for InMemoryCache {
976    fn default() -> Self {
977        Self {
978            limit: DEFAULT_CACHE_CAPACITY,
979        }
980    }
981}
982
983#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
984#[serde(deny_unknown_fields)]
985/// Redis cache configuration
986pub(crate) struct RedisCache {
987    /// List of URLs to the Redis cluster
988    pub(crate) urls: Vec<url::Url>,
989
990    /// Redis username if not provided in the URLs. This field takes precedence over the username in the URL
991    pub(crate) username: Option<String>,
992    /// Redis password if not provided in the URLs. This field takes precedence over the password in the URL
993    pub(crate) password: Option<String>,
994
995    #[serde(deserialize_with = "humantime_serde::deserialize", default)]
996    #[schemars(with = "Option<String>", default)]
997    /// Redis request timeout (default: 2ms)
998    pub(crate) timeout: Option<Duration>,
999
1000    #[serde(deserialize_with = "humantime_serde::deserialize", default)]
1001    #[schemars(with = "Option<String>", default)]
1002    /// TTL for entries
1003    pub(crate) ttl: Option<Duration>,
1004
1005    /// namespace used to prefix Redis keys
1006    pub(crate) namespace: Option<String>,
1007
1008    #[serde(default)]
1009    /// TLS client configuration
1010    pub(crate) tls: Option<TlsClient>,
1011
1012    #[serde(default = "default_required_to_start")]
1013    /// Prevents the router from starting if it cannot connect to Redis
1014    pub(crate) required_to_start: bool,
1015
1016    #[serde(default = "default_reset_ttl")]
1017    /// When a TTL is set on a key, reset it when reading the data from that key
1018    pub(crate) reset_ttl: bool,
1019
1020    #[serde(default = "default_pool_size")]
1021    /// The size of the Redis connection pool
1022    pub(crate) pool_size: u32,
1023}
1024
1025fn default_required_to_start() -> bool {
1026    false
1027}
1028
1029fn default_pool_size() -> u32 {
1030    1
1031}
1032
1033impl From<QueryPlanRedisCache> for RedisCache {
1034    fn from(value: QueryPlanRedisCache) -> Self {
1035        RedisCache {
1036            urls: value.urls,
1037            username: value.username,
1038            password: value.password,
1039            timeout: value.timeout,
1040            ttl: Some(value.ttl),
1041            namespace: value.namespace,
1042            tls: value.tls,
1043            required_to_start: value.required_to_start,
1044            reset_ttl: value.reset_ttl,
1045            pool_size: value.pool_size,
1046        }
1047    }
1048}
1049
1050fn default_reset_ttl() -> bool {
1051    true
1052}
1053
1054/// TLS related configuration options.
1055#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema)]
1056#[serde(deny_unknown_fields)]
1057#[serde(default)]
1058pub(crate) struct Tls {
1059    /// TLS server configuration
1060    ///
1061    /// this will affect the GraphQL endpoint and any other endpoint targeting the same listen address
1062    pub(crate) supergraph: Option<TlsSupergraph>,
1063    pub(crate) subgraph: SubgraphConfiguration<TlsClient>,
1064}
1065
1066/// Configuration options pertaining to the supergraph server component.
1067#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
1068#[serde(deny_unknown_fields)]
1069pub(crate) struct TlsSupergraph {
1070    /// server certificate in PEM format
1071    #[serde(deserialize_with = "deserialize_certificate", skip_serializing)]
1072    #[schemars(with = "String")]
1073    pub(crate) certificate: Certificate,
1074    /// server key in PEM format
1075    #[serde(deserialize_with = "deserialize_key", skip_serializing)]
1076    #[schemars(with = "String")]
1077    pub(crate) key: PrivateKey,
1078    /// list of certificate authorities in PEM format
1079    #[serde(deserialize_with = "deserialize_certificate_chain", skip_serializing)]
1080    #[schemars(with = "String")]
1081    pub(crate) certificate_chain: Vec<Certificate>,
1082}
1083
1084impl TlsSupergraph {
1085    pub(crate) fn tls_config(&self) -> Result<Arc<rustls::ServerConfig>, ApolloRouterError> {
1086        let mut certificates = vec![self.certificate.clone()];
1087        certificates.extend(self.certificate_chain.iter().cloned());
1088
1089        let mut config = ServerConfig::builder()
1090            .with_safe_defaults()
1091            .with_no_client_auth()
1092            .with_single_cert(certificates, self.key.clone())
1093            .map_err(ApolloRouterError::Rustls)?;
1094        config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
1095
1096        Ok(Arc::new(config))
1097    }
1098}
1099
1100fn deserialize_certificate<'de, D>(deserializer: D) -> Result<Certificate, D::Error>
1101where
1102    D: Deserializer<'de>,
1103{
1104    let data = String::deserialize(deserializer)?;
1105
1106    load_certs(&data)
1107        .map_err(serde::de::Error::custom)
1108        .and_then(|mut certs| {
1109            if certs.len() > 1 {
1110                Err(serde::de::Error::custom("expected exactly one certificate"))
1111            } else {
1112                certs
1113                    .pop()
1114                    .ok_or(serde::de::Error::custom("expected exactly one certificate"))
1115            }
1116        })
1117}
1118
1119fn deserialize_certificate_chain<'de, D>(deserializer: D) -> Result<Vec<Certificate>, D::Error>
1120where
1121    D: Deserializer<'de>,
1122{
1123    let data = String::deserialize(deserializer)?;
1124
1125    load_certs(&data).map_err(serde::de::Error::custom)
1126}
1127
1128fn deserialize_key<'de, D>(deserializer: D) -> Result<PrivateKey, D::Error>
1129where
1130    D: Deserializer<'de>,
1131{
1132    let data = String::deserialize(deserializer)?;
1133
1134    load_key(&data).map_err(serde::de::Error::custom)
1135}
1136
1137pub(crate) fn load_certs(data: &str) -> io::Result<Vec<Certificate>> {
1138    certs(&mut BufReader::new(data.as_bytes()))
1139        .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid cert"))
1140        .map(|mut certs| certs.drain(..).map(Certificate).collect())
1141}
1142
1143pub(crate) fn load_key(data: &str) -> io::Result<PrivateKey> {
1144    let mut reader = BufReader::new(data.as_bytes());
1145    let mut key_iterator = iter::from_fn(|| read_one(&mut reader).transpose());
1146
1147    let private_key = match key_iterator.next() {
1148        Some(Ok(Item::RSAKey(key))) => PrivateKey(key),
1149        Some(Ok(Item::PKCS8Key(key))) => PrivateKey(key),
1150        Some(Ok(Item::ECKey(key))) => PrivateKey(key),
1151        Some(Err(e)) => {
1152            return Err(io::Error::new(
1153                io::ErrorKind::InvalidInput,
1154                format!("could not parse the key: {e}"),
1155            ));
1156        }
1157        Some(_) => {
1158            return Err(io::Error::new(
1159                io::ErrorKind::InvalidInput,
1160                "expected a private key",
1161            ));
1162        }
1163        None => {
1164            return Err(io::Error::new(
1165                io::ErrorKind::InvalidInput,
1166                "could not find a private key",
1167            ));
1168        }
1169    };
1170
1171    if key_iterator.next().is_some() {
1172        return Err(io::Error::new(
1173            io::ErrorKind::InvalidInput,
1174            "expected exactly one private key",
1175        ));
1176    }
1177    Ok(private_key)
1178}
1179
1180/// Configuration options pertaining to the subgraph server component.
1181#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
1182#[serde(deny_unknown_fields)]
1183#[serde(default)]
1184pub(crate) struct TlsClient {
1185    /// list of certificate authorities in PEM format
1186    pub(crate) certificate_authorities: Option<String>,
1187    /// client certificate authentication
1188    pub(crate) client_authentication: Option<TlsClientAuth>,
1189}
1190
1191#[buildstructor::buildstructor]
1192impl TlsClient {
1193    #[builder]
1194    pub(crate) fn new(
1195        certificate_authorities: Option<String>,
1196        client_authentication: Option<TlsClientAuth>,
1197    ) -> Self {
1198        Self {
1199            certificate_authorities,
1200            client_authentication,
1201        }
1202    }
1203}
1204
1205impl Default for TlsClient {
1206    fn default() -> Self {
1207        Self::builder().build()
1208    }
1209}
1210
1211/// TLS client authentication
1212#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
1213#[serde(deny_unknown_fields)]
1214pub(crate) struct TlsClientAuth {
1215    /// list of certificates in PEM format
1216    #[serde(deserialize_with = "deserialize_certificate_chain", skip_serializing)]
1217    #[schemars(with = "String")]
1218    pub(crate) certificate_chain: Vec<Certificate>,
1219    /// key in PEM format
1220    #[serde(deserialize_with = "deserialize_key", skip_serializing)]
1221    #[schemars(with = "String")]
1222    pub(crate) key: PrivateKey,
1223}
1224
1225/// Configuration options pertaining to the sandbox page.
1226#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
1227#[serde(deny_unknown_fields)]
1228#[serde(default)]
1229pub(crate) struct Sandbox {
1230    /// Set to true to enable sandbox
1231    pub(crate) enabled: bool,
1232}
1233
1234fn default_sandbox() -> bool {
1235    false
1236}
1237
1238#[buildstructor::buildstructor]
1239impl Sandbox {
1240    #[builder]
1241    pub(crate) fn new(enabled: Option<bool>) -> Self {
1242        Self {
1243            enabled: enabled.unwrap_or_else(default_sandbox),
1244        }
1245    }
1246}
1247
1248#[cfg(test)]
1249#[buildstructor::buildstructor]
1250impl Sandbox {
1251    #[builder]
1252    pub(crate) fn fake_new(enabled: Option<bool>) -> Self {
1253        Self {
1254            enabled: enabled.unwrap_or_else(default_sandbox),
1255        }
1256    }
1257}
1258
1259impl Default for Sandbox {
1260    fn default() -> Self {
1261        Self::builder().build()
1262    }
1263}
1264
1265/// Configuration options pertaining to the home page.
1266#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
1267#[serde(deny_unknown_fields)]
1268#[serde(default)]
1269pub(crate) struct Homepage {
1270    /// Set to false to disable the homepage
1271    pub(crate) enabled: bool,
1272    /// Graph reference
1273    /// This will allow you to redirect from the Apollo Router landing page back to Apollo Studio Explorer
1274    pub(crate) graph_ref: Option<String>,
1275}
1276
1277fn default_homepage() -> bool {
1278    true
1279}
1280
1281#[buildstructor::buildstructor]
1282impl Homepage {
1283    #[builder]
1284    pub(crate) fn new(enabled: Option<bool>) -> Self {
1285        Self {
1286            enabled: enabled.unwrap_or_else(default_homepage),
1287            graph_ref: None,
1288        }
1289    }
1290}
1291
1292#[cfg(test)]
1293#[buildstructor::buildstructor]
1294impl Homepage {
1295    #[builder]
1296    pub(crate) fn fake_new(enabled: Option<bool>) -> Self {
1297        Self {
1298            enabled: enabled.unwrap_or_else(default_homepage),
1299            graph_ref: None,
1300        }
1301    }
1302}
1303
1304impl Default for Homepage {
1305    fn default() -> Self {
1306        Self::builder().enabled(default_homepage()).build()
1307    }
1308}
1309
1310/// Configuration options pertaining to the http server component.
1311#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
1312#[serde(deny_unknown_fields)]
1313#[serde(default)]
1314pub(crate) struct HealthCheck {
1315    /// The socket address and port to listen on
1316    /// Defaults to 127.0.0.1:8088
1317    pub(crate) listen: ListenAddr,
1318
1319    /// Set to false to disable the health check
1320    pub(crate) enabled: bool,
1321
1322    /// Optionally set a custom healthcheck path
1323    /// Defaults to /health
1324    pub(crate) path: String,
1325}
1326
1327fn default_health_check_listen() -> ListenAddr {
1328    SocketAddr::from_str("127.0.0.1:8088").unwrap().into()
1329}
1330
1331fn default_health_check_enabled() -> bool {
1332    true
1333}
1334
1335fn default_health_check_path() -> String {
1336    "/health".to_string()
1337}
1338
1339#[buildstructor::buildstructor]
1340impl HealthCheck {
1341    #[builder]
1342    pub(crate) fn new(
1343        listen: Option<ListenAddr>,
1344        enabled: Option<bool>,
1345        path: Option<String>,
1346    ) -> Self {
1347        let mut path = path.unwrap_or_else(default_health_check_path);
1348        if !path.starts_with('/') {
1349            path = format!("/{path}").to_string();
1350        }
1351
1352        Self {
1353            listen: listen.unwrap_or_else(default_health_check_listen),
1354            enabled: enabled.unwrap_or_else(default_health_check_enabled),
1355            path,
1356        }
1357    }
1358}
1359
1360#[cfg(test)]
1361#[buildstructor::buildstructor]
1362impl HealthCheck {
1363    #[builder]
1364    pub(crate) fn fake_new(
1365        listen: Option<ListenAddr>,
1366        enabled: Option<bool>,
1367        path: Option<String>,
1368    ) -> Self {
1369        let mut path = path.unwrap_or_else(default_health_check_path);
1370        if !path.starts_with('/') {
1371            path = format!("/{path}");
1372        }
1373
1374        Self {
1375            listen: listen.unwrap_or_else(test_listen),
1376            enabled: enabled.unwrap_or_else(default_health_check_enabled),
1377            path,
1378        }
1379    }
1380}
1381
1382impl Default for HealthCheck {
1383    fn default() -> Self {
1384        Self::builder().build()
1385    }
1386}
1387
1388/// Configuration for chaos testing, trying to reproduce bugs that require uncommon conditions.
1389/// You probably don’t want this in production!
1390#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema)]
1391#[serde(deny_unknown_fields)]
1392#[serde(default)]
1393pub(crate) struct Chaos {
1394    /// Force a hot reload of the Router (as if the schema or configuration had changed)
1395    /// at a regular time interval.
1396    #[serde(with = "humantime_serde")]
1397    #[schemars(with = "Option<String>")]
1398    pub(crate) force_reload: Option<std::time::Duration>,
1399}
1400
1401/// Listening address.
1402#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize, JsonSchema)]
1403#[serde(untagged)]
1404pub enum ListenAddr {
1405    /// Socket address.
1406    SocketAddr(SocketAddr),
1407    /// Unix socket.
1408    #[cfg(unix)]
1409    UnixSocket(std::path::PathBuf),
1410}
1411
1412impl ListenAddr {
1413    pub(crate) fn ip_and_port(&self) -> Option<(IpAddr, u16)> {
1414        #[cfg_attr(not(unix), allow(irrefutable_let_patterns))]
1415        if let Self::SocketAddr(addr) = self {
1416            Some((addr.ip(), addr.port()))
1417        } else {
1418            None
1419        }
1420    }
1421}
1422
1423impl From<SocketAddr> for ListenAddr {
1424    fn from(addr: SocketAddr) -> Self {
1425        Self::SocketAddr(addr)
1426    }
1427}
1428
1429#[allow(clippy::from_over_into)]
1430impl Into<serde_json::Value> for ListenAddr {
1431    fn into(self) -> serde_json::Value {
1432        match self {
1433            // It avoids to prefix with `http://` when serializing and relying on the Display impl.
1434            // Otherwise, it's converted to a `UnixSocket` in any case.
1435            Self::SocketAddr(addr) => serde_json::Value::String(addr.to_string()),
1436            #[cfg(unix)]
1437            Self::UnixSocket(path) => serde_json::Value::String(
1438                path.as_os_str()
1439                    .to_str()
1440                    .expect("unsupported non-UTF-8 path")
1441                    .to_string(),
1442            ),
1443        }
1444    }
1445}
1446
1447#[cfg(unix)]
1448impl From<tokio_util::either::Either<std::net::SocketAddr, tokio::net::unix::SocketAddr>>
1449    for ListenAddr
1450{
1451    fn from(
1452        addr: tokio_util::either::Either<std::net::SocketAddr, tokio::net::unix::SocketAddr>,
1453    ) -> Self {
1454        match addr {
1455            tokio_util::either::Either::Left(addr) => Self::SocketAddr(addr),
1456            tokio_util::either::Either::Right(addr) => Self::UnixSocket(
1457                addr.as_pathname()
1458                    .map(ToOwned::to_owned)
1459                    .unwrap_or_default(),
1460            ),
1461        }
1462    }
1463}
1464
1465impl fmt::Display for ListenAddr {
1466    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1467        match self {
1468            Self::SocketAddr(addr) => write!(f, "http://{addr}"),
1469            #[cfg(unix)]
1470            Self::UnixSocket(path) => write!(f, "{}", path.display()),
1471        }
1472    }
1473}
1474
1475fn default_graphql_path() -> String {
1476    String::from("/")
1477}
1478
1479fn default_graphql_introspection() -> bool {
1480    false
1481}
1482
1483fn default_connection_shutdown_timeout() -> Duration {
1484    Duration::from_secs(60)
1485}
1486
1487#[derive(Clone, Debug, Default, Error, Display, Serialize, Deserialize, JsonSchema)]
1488#[serde(deny_unknown_fields, rename_all = "snake_case")]
1489pub(crate) enum BatchingMode {
1490    /// batch_http_link
1491    #[default]
1492    BatchHttpLink,
1493}
1494
1495/// Configuration for Batching
1496#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema)]
1497#[serde(deny_unknown_fields)]
1498pub(crate) struct Batching {
1499    /// Activates Batching (disabled by default)
1500    #[serde(default)]
1501    pub(crate) enabled: bool,
1502
1503    /// Batching mode
1504    pub(crate) mode: BatchingMode,
1505
1506    /// Subgraph options for batching
1507    pub(crate) subgraph: Option<SubgraphConfiguration<CommonBatchingConfig>>,
1508
1509    /// Maximum size for a batch
1510    #[serde(default)]
1511    pub(crate) maximum_size: Option<usize>,
1512}
1513
1514/// Common options for configuring subgraph batching
1515#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema)]
1516pub(crate) struct CommonBatchingConfig {
1517    /// Whether this batching config should be enabled
1518    pub(crate) enabled: bool,
1519}
1520
1521impl Batching {
1522    // Check if we should enable batching for a particular subgraph (service_name)
1523    pub(crate) fn batch_include(&self, service_name: &str) -> bool {
1524        match &self.subgraph {
1525            Some(subgraph_batching_config) => {
1526                // Override by checking if all is enabled
1527                if subgraph_batching_config.all.enabled {
1528                    // If it is, require:
1529                    // - no subgraph entry OR
1530                    // - an enabled subgraph entry
1531                    subgraph_batching_config
1532                        .subgraphs
1533                        .get(service_name)
1534                        .is_none_or(|x| x.enabled)
1535                } else {
1536                    // If it isn't, require:
1537                    // - an enabled subgraph entry
1538                    subgraph_batching_config
1539                        .subgraphs
1540                        .get(service_name)
1541                        .is_some_and(|x| x.enabled)
1542                }
1543            }
1544            None => false,
1545        }
1546    }
1547
1548    pub(crate) fn exceeds_batch_size<T>(&self, batch: &[T]) -> bool {
1549        match self.maximum_size {
1550            Some(maximum_size) => batch.len() > maximum_size,
1551            None => false,
1552        }
1553    }
1554}