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