1use 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
85static 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#[derive(Debug, Error, Display)]
98#[non_exhaustive]
99pub enum ConfigurationError {
100 CannotExpandVariable { key: String, cause: String },
102 UnknownExpansionMode {
104 key: String,
105 supported_modes: String,
106 },
107 PluginUnknown(String),
109 PluginConfiguration { plugin: String, error: String },
111 InvalidConfiguration {
113 message: &'static str,
114 error: String,
115 },
116 DeserializeConfigError(serde_json::Error),
118
119 InvalidExpansionModeConfig,
121
122 MigrationFailure { error: String },
124
125 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#[derive(Clone, Derivative, Serialize, JsonSchema)]
150#[derivative(Debug)]
151pub struct Configuration {
153 #[serde(skip)]
155 pub(crate) validated_yaml: Option<Value>,
156
157 #[serde(skip)]
159 pub(crate) raw_yaml: Option<Arc<str>>,
160
161 #[serde(default)]
163 pub(crate) reload: Reload,
164
165 #[serde(default)]
167 pub(crate) health_check: HealthCheck,
168
169 #[serde(default)]
171 pub(crate) sandbox: Sandbox,
172
173 #[serde(default)]
175 pub(crate) homepage: Homepage,
176
177 #[serde(default)]
179 pub(crate) server: Server,
180
181 #[serde(default)]
183 pub(crate) supergraph: Supergraph,
184
185 #[serde(default)]
187 pub(crate) cors: Cors,
188
189 #[serde(default)]
190 pub(crate) tls: Tls,
191
192 #[serde(default)]
194 pub(crate) apq: Apq,
195
196 #[serde(default)]
198 pub persisted_queries: PersistedQueries,
199
200 #[serde(default)]
202 pub(crate) limits: limits::Config,
203
204 #[serde(default)]
207 pub(crate) experimental_chaos: Config,
208
209 #[serde(default)]
211 pub(crate) plugins: UserPlugins,
212
213 #[serde(default)]
215 #[serde(flatten)]
216 pub(crate) apollo_plugins: ApolloPlugins,
217
218 #[serde(skip)]
220 pub uplink: Option<UplinkConfig>,
221
222 #[serde(default, skip_serializing, skip_deserializing)]
225 pub(crate) notify: Notify<String, graphql::Response>,
226
227 #[serde(default)]
229 pub(crate) batching: Batching,
230
231 #[serde(default)]
233 pub(crate) experimental_type_conditioned_fetching: bool,
234
235 #[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 #[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 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 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 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 .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 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 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 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 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 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 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
627impl 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 .map(|(k, v)| (format!("^{}$", k), v))
650 .collect::<BTreeMap<_, _>>()
651 })
652}
653
654#[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 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 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#[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 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#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
724#[serde(deny_unknown_fields)]
725#[serde(default)]
726pub(crate) struct Supergraph {
727 pub(crate) listen: ListenAddr,
730
731 #[serde(deserialize_with = "humantime_serde::deserialize")]
733 #[schemars(with = "String", default = "default_connection_shutdown_timeout")]
734 pub(crate) connection_shutdown_timeout: Duration,
735
736 pub(crate) path: String,
739
740 pub(crate) introspection: bool,
743
744 pub(crate) redact_query_validation_errors: bool,
748
749 pub(crate) generate_query_fragments: bool,
752
753 pub(crate) defer_support: bool,
755
756 pub(crate) query_planning: QueryPlanning,
758
759 pub(crate) early_cancel: bool,
764
765 pub(crate) enable_result_coercion_errors: bool,
772
773 pub(crate) experimental_log_on_broken_pipe: bool,
776
777 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 pub(crate) fn sanitized_path(&self) -> String {
875 let mut path = self.path.clone();
876 if self.path.ends_with("/*") {
877 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#[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#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
906#[serde(deny_unknown_fields, default)]
907pub(crate) struct Apq {
908 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#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema)]
930#[serde(deny_unknown_fields, default)]
931pub(crate) struct SubgraphApq {
932 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#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, Default)]
952#[serde(deny_unknown_fields, default)]
953pub(crate) struct QueryPlanning {
954 pub(crate) cache: QueryPlanCache,
956 pub(crate) warmed_up_queries: Option<usize>,
961
962 pub(crate) experimental_plans_limit: Option<u32>,
970
971 pub(crate) experimental_paths_limit: Option<u32>,
984
985 pub(crate) experimental_reuse_query_plans: bool,
988
989 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#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema)]
1021#[serde(deny_unknown_fields, default)]
1022pub(crate) struct QueryPlanCache {
1023 pub(crate) in_memory: InMemoryCache,
1025 pub(crate) redis: Option<QueryPlanRedisCache>,
1027}
1028
1029#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
1030#[serde(deny_unknown_fields)]
1031pub(crate) struct QueryPlanRedisCache {
1033 pub(crate) urls: Vec<url::Url>,
1035
1036 pub(crate) username: Option<String>,
1038 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 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 pub(crate) ttl: Duration,
1056
1057 pub(crate) namespace: Option<String>,
1059
1060 #[serde(default)]
1061 pub(crate) tls: Option<TlsClient>,
1063
1064 #[serde(default = "default_required_to_start")]
1065 pub(crate) required_to_start: bool,
1067
1068 #[serde(default = "default_reset_ttl")]
1069 pub(crate) reset_ttl: bool,
1071
1072 #[serde(default = "default_query_planner_cache_pool_size")]
1073 pub(crate) pool_size: u32,
1075}
1076
1077fn default_query_plan_cache_ttl() -> Duration {
1078 Duration::from_secs(86400 * 30)
1080}
1081
1082fn default_query_planner_cache_pool_size() -> u32 {
1083 1
1084}
1085
1086#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema)]
1088#[serde(deny_unknown_fields, default)]
1089pub(crate) struct Cache {
1090 pub(crate) in_memory: InMemoryCache,
1092 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)]
1107pub(crate) struct InMemoryCache {
1109 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)]
1123pub(crate) struct RedisCache {
1125 pub(crate) urls: Vec<url::Url>,
1127
1128 pub(crate) username: Option<String>,
1130 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 pub(crate) timeout: Duration,
1140
1141 #[serde(deserialize_with = "humantime_serde::deserialize", default)]
1142 #[schemars(with = "Option<String>", default)]
1143 pub(crate) ttl: Option<Duration>,
1145
1146 pub(crate) namespace: Option<String>,
1148
1149 #[serde(default)]
1150 pub(crate) tls: Option<TlsClient>,
1152
1153 #[serde(default = "default_required_to_start")]
1154 pub(crate) required_to_start: bool,
1156
1157 #[serde(default = "default_reset_ttl")]
1158 pub(crate) reset_ttl: bool,
1160
1161 #[serde(default = "default_pool_size")]
1162 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 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#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema)]
1213#[serde(deny_unknown_fields)]
1214#[serde(default)]
1215pub(crate) struct Tls {
1216 pub(crate) supergraph: Option<Arc<TlsSupergraph>>,
1220 pub(crate) subgraph: SubgraphConfiguration<TlsClient>,
1222 pub(crate) connector: ConnectorConfiguration<TlsClient>,
1224}
1225
1226#[derive(Debug, Deserialize, Serialize, JsonSchema)]
1228#[serde(deny_unknown_fields)]
1229pub(crate) struct TlsSupergraph {
1230 #[serde(deserialize_with = "deserialize_certificate", skip_serializing)]
1232 #[schemars(with = "String")]
1233 pub(crate) certificate: CertificateDer<'static>,
1234 #[serde(deserialize_with = "deserialize_key", skip_serializing)]
1236 #[schemars(with = "String")]
1237 pub(crate) key: PrivateKeyDer<'static>,
1238 #[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#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, PartialEq)]
1347#[serde(deny_unknown_fields)]
1348#[serde(default)]
1349pub(crate) struct TlsClient {
1350 pub(crate) certificate_authorities: Option<String>,
1352 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#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
1378#[serde(deny_unknown_fields)]
1379pub(crate) struct TlsClientAuth {
1380 #[serde(deserialize_with = "deserialize_certificate_chain", skip_serializing)]
1382 #[schemars(with = "String")]
1383 pub(crate) certificate_chain: Vec<CertificateDer<'static>>,
1384 #[serde(deserialize_with = "deserialize_key", skip_serializing)]
1386 #[schemars(with = "String")]
1387 pub(crate) key: PrivateKeyDer<'static>,
1388}
1389
1390#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
1392#[serde(deny_unknown_fields)]
1393#[serde(default)]
1394pub(crate) struct Reload {
1395 #[schemars(default = "default_reload_max_retries")]
1401 pub(crate) max_retries: Option<u32>,
1402
1403 #[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#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
1429#[serde(deny_unknown_fields)]
1430#[serde(default)]
1431pub(crate) struct Sandbox {
1432 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#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
1469#[serde(deny_unknown_fields)]
1470#[serde(default)]
1471pub(crate) struct Homepage {
1472 pub(crate) enabled: bool,
1474 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#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize, JsonSchema)]
1514#[serde(untagged)]
1515pub enum ListenAddr {
1516 SocketAddr(SocketAddr),
1518 #[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 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 #[default]
1607 BatchHttpLink,
1608}
1609
1610#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema)]
1612#[serde(deny_unknown_fields, default)]
1613pub(crate) struct Batching {
1614 pub(crate) enabled: bool,
1616
1617 pub(crate) mode: BatchingMode,
1619
1620 pub(crate) subgraph: Option<SubgraphConfiguration<CommonBatchingConfig>>,
1622
1623 pub(crate) maximum_size: Option<usize>,
1625}
1626
1627#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema)]
1629#[serde(deny_unknown_fields, default)]
1630pub(crate) struct CommonBatchingConfig {
1631 pub(crate) enabled: bool,
1633}
1634
1635impl Batching {
1636 pub(crate) fn batch_include(&self, service_name: &str) -> bool {
1638 match &self.subgraph {
1639 Some(subgraph_batching_config) => {
1640 if subgraph_batching_config.all.enabled {
1642 subgraph_batching_config
1646 .subgraphs
1647 .get(service_name)
1648 .is_none_or(|x| x.enabled)
1649 } else {
1650 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#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema)]
1677#[serde(deny_unknown_fields)]
1678pub(crate) struct HoistOrphanErrors {
1679 #[serde(default)]
1681 pub(crate) enabled: bool,
1682}