Skip to main content

apollo_router/configuration/
mod.rs

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