diff --git a/.changesets/config_header_read_timeout.md b/.changesets/config_header_read_timeout.md new file mode 100644 index 0000000000..fa006ae661 --- /dev/null +++ b/.changesets/config_header_read_timeout.md @@ -0,0 +1,13 @@ +### Add configurable server header read timeout ([PR #7262](https://github.com/apollographql/router/pull/7262)) + +This change exposes the server's header read timeout as the `server.http.header_read_timeout` configuration option. + +By default, the `server.http.header_read_timeout` is set to previously hard-coded 10 seconds. A longer timeout can be configured using the `server.http.header_read_timeout` option. + +```yaml title="router.yaml" +server: + http: + header_read_timeout: 30s +``` + +By [@gwardwell ](https://github.com/gwardwell) in https://github.com/apollographql/router/pull/7262 diff --git a/apollo-router/src/axum_factory/axum_http_server_factory.rs b/apollo-router/src/axum_factory/axum_http_server_factory.rs index 18ad8fe8ae..216824e293 100644 --- a/apollo-router/src/axum_factory/axum_http_server_factory.rs +++ b/apollo-router/src/axum_factory/axum_http_server_factory.rs @@ -5,7 +5,6 @@ use std::sync::Arc; use std::sync::atomic::AtomicBool; use std::sync::atomic::AtomicU64; use std::sync::atomic::Ordering; -use std::time::Duration; use std::time::Instant; use axum::Router; @@ -316,7 +315,7 @@ impl HttpServerFactory for AxumHttpServerFactory { .map_err(ApolloRouterError::ServerCreationError)?; let mut http_config = Http::new(); http_config.http1_keep_alive(true); - http_config.http1_header_read_timeout(Duration::from_secs(10)); + http_config.http1_header_read_timeout(configuration.server.http.header_read_timeout); #[cfg(feature = "hyper_header_limits")] if let Some(max_headers) = configuration.limits.http1_max_request_headers { diff --git a/apollo-router/src/configuration/mod.rs b/apollo-router/src/configuration/mod.rs index 7844409859..3efc7db885 100644 --- a/apollo-router/src/configuration/mod.rs +++ b/apollo-router/src/configuration/mod.rs @@ -45,6 +45,7 @@ use self::expansion::Expansion; pub(crate) use self::experimental::Discussed; pub(crate) use self::schema::generate_config_schema; pub(crate) use self::schema::generate_upgrade; +use self::server::Server; use self::subgraph::SubgraphConfiguration; use crate::ApolloRouterError; use crate::cache::DEFAULT_CACHE_CAPACITY; @@ -64,6 +65,7 @@ mod experimental; pub(crate) mod metrics; mod persisted_queries; mod schema; +pub(crate) mod server; pub(crate) mod shared; pub(crate) mod subgraph; #[cfg(test)] @@ -136,6 +138,10 @@ pub struct Configuration { #[serde(default)] pub(crate) homepage: Homepage, + /// Configuration for the server + #[serde(default)] + pub(crate) server: Server, + /// Configuration for the supergraph #[serde(default)] pub(crate) supergraph: Supergraph, @@ -208,6 +214,7 @@ impl<'de> serde::Deserialize<'de> for Configuration { health_check: HealthCheck, sandbox: Sandbox, homepage: Homepage, + server: Server, supergraph: Supergraph, cors: Cors, plugins: UserPlugins, @@ -238,6 +245,7 @@ impl<'de> serde::Deserialize<'de> for Configuration { health_check: ad_hoc.health_check, sandbox: ad_hoc.sandbox, homepage: ad_hoc.homepage, + server: ad_hoc.server, supergraph: ad_hoc.supergraph, cors: ad_hoc.cors, tls: ad_hoc.tls, @@ -291,12 +299,14 @@ impl Configuration { uplink: Option, experimental_type_conditioned_fetching: Option, batching: Option, + server: Option, ) -> Result { let notify = Self::notify(&apollo_plugins)?; let conf = Self { validated_yaml: Default::default(), supergraph: supergraph.unwrap_or_default(), + server: server.unwrap_or_default(), health_check: health_check.unwrap_or_default(), sandbox: sandbox.unwrap_or_default(), homepage: homepage.unwrap_or_default(), @@ -426,9 +436,11 @@ impl Configuration { uplink: Option, batching: Option, experimental_type_conditioned_fetching: Option, + server: Option, ) -> Result { let configuration = Self { validated_yaml: Default::default(), + server: server.unwrap_or_default(), supergraph: supergraph.unwrap_or_else(|| Supergraph::fake_builder().build()), health_check: health_check.unwrap_or_else(|| HealthCheck::fake_builder().build()), sandbox: sandbox.unwrap_or_else(|| Sandbox::fake_builder().build()), diff --git a/apollo-router/src/configuration/server.rs b/apollo-router/src/configuration/server.rs new file mode 100644 index 0000000000..a2ed812671 --- /dev/null +++ b/apollo-router/src/configuration/server.rs @@ -0,0 +1,118 @@ +use std::time::Duration; + +use schemars::JsonSchema; +use serde::Deserialize; +use serde::Serialize; + +const DEFAULT_HEADER_READ_TIMEOUT: Duration = Duration::from_secs(10); + +fn default_header_read_timeout() -> Duration { + DEFAULT_HEADER_READ_TIMEOUT +} + +/// Configuration for HTTP +#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)] +#[serde(deny_unknown_fields, default)] +pub(crate) struct ServerHttpConfig { + /// Header read timeout in human-readable format; defaults to 10s + #[serde( + deserialize_with = "humantime_serde::deserialize", + default = "default_header_read_timeout" + )] + #[schemars(with = "String", default = "default_header_read_timeout")] + pub(crate) header_read_timeout: Duration, +} + +#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)] +#[serde(deny_unknown_fields, default)] +pub(crate) struct Server { + /// The server http configuration + pub(crate) http: ServerHttpConfig, +} + +impl Default for ServerHttpConfig { + fn default() -> Self { + Self { + header_read_timeout: Duration::from_secs(10), + } + } +} + +#[buildstructor::buildstructor] +impl Server { + #[builder] + pub(crate) fn new(http: Option) -> Self { + Self { + http: http.unwrap_or_default(), + } + } +} + +impl Default for Server { + fn default() -> Self { + Self::builder().build() + } +} + +#[cfg(test)] +mod tests { + use serde_json::json; + + use super::*; + + #[test] + fn it_builds_default_server_configuration() { + let default_duration_seconds = Duration::from_secs(10); + let server_config = Server::builder().build(); + assert_eq!( + server_config.http.header_read_timeout, + default_duration_seconds + ); + } + + #[test] + fn it_json_parses_default_header_read_timeout_when_server_http_config_omitted() { + let json_server = json!({}); + + let config: Server = serde_json::from_value(json_server).unwrap(); + + assert_eq!(config.http.header_read_timeout, Duration::from_secs(10)); + } + + #[test] + fn it_json_parses_default_header_read_timeout_when_omitted() { + let json_config = json!({ + "http": {} + }); + + let config: Server = serde_json::from_value(json_config).unwrap(); + + assert_eq!(config.http.header_read_timeout, Duration::from_secs(10)); + } + + #[test] + fn it_json_parses_specified_server_config_seconds_correctly() { + let json_config = json!({ + "http": { + "header_read_timeout": "30s" + } + }); + + let config: Server = serde_json::from_value(json_config).unwrap(); + + assert_eq!(config.http.header_read_timeout, Duration::from_secs(30)); + } + + #[test] + fn it_json_parses_specified_server_config_minutes_correctly() { + let json_config = json!({ + "http": { + "header_read_timeout": "1m" + } + }); + + let config: Server = serde_json::from_value(json_config).unwrap(); + + assert_eq!(config.http.header_read_timeout, Duration::from_secs(60)); + } +} diff --git a/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap b/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap index 014b8148c8..37f70fc2d8 100644 --- a/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap +++ b/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap @@ -5326,6 +5326,31 @@ expression: "&schema" } ] }, + "Server": { + "additionalProperties": false, + "properties": { + "http": { + "$ref": "#/definitions/ServerHttpConfig", + "description": "#/definitions/ServerHttpConfig" + } + }, + "type": "object" + }, + "ServerHttpConfig": { + "additionalProperties": false, + "description": "Configuration for HTTP", + "properties": { + "header_read_timeout": { + "default": { + "nanos": 0, + "secs": 10 + }, + "description": "Header read timeout in human-readable format; defaults to 10s", + "type": "string" + } + }, + "type": "object" + }, "SocketEndpoint": { "type": "string" }, @@ -8359,6 +8384,10 @@ expression: "&schema" "$ref": "#/definitions/Sandbox", "description": "#/definitions/Sandbox" }, + "server": { + "$ref": "#/definitions/Server", + "description": "#/definitions/Server" + }, "subscription": { "$ref": "#/definitions/SubscriptionConfig", "description": "#/definitions/SubscriptionConfig" diff --git a/apollo-router/src/configuration/tests.rs b/apollo-router/src/configuration/tests.rs index a86a374041..6dd96588cd 100644 --- a/apollo-router/src/configuration/tests.rs +++ b/apollo-router/src/configuration/tests.rs @@ -1116,6 +1116,36 @@ fn it_processes_specified_maximum_batch_limit_correctly() { assert_eq!(config.maximum_size, Some(10)); } +#[test] +fn it_includes_default_header_read_timeout_when_server_config_omitted() { + let json_config = json!({}); + + let config: Configuration = serde_json::from_value(json_config).unwrap(); + + assert_eq!( + config.server.http.header_read_timeout, + Duration::from_secs(10) + ); +} + +#[test] +fn it_processes_specified_server_config_correctly() { + let json_config = json!({ + "server": { + "http": { + "header_read_timeout": "30s" + } + } + }); + + let config: Configuration = serde_json::from_value(json_config).unwrap(); + + assert_eq!( + config.server.http.header_read_timeout, + Duration::from_secs(30) + ); +} + fn has_field_level_serde_defaults(lines: &[&str], line_number: usize) -> bool { let serde_field_default = Regex::new( r#"^\s*#[\s\n]*\[serde\s*\((.*,)?\s*default\s*=\s*"[a-zA-Z0-9_:]+"\s*(,.*)?\)\s*\]\s*$"#, diff --git a/docs/source/routing/configuration.mdx b/docs/source/routing/configuration.mdx index 2454f5c041..68a7a1758d 100644 --- a/docs/source/routing/configuration.mdx +++ b/docs/source/routing/configuration.mdx @@ -1211,6 +1211,18 @@ traffic_shaping: timeout: 60s ``` +### Header Read Timeout + +The header read timeout is the amount of time the Router will wait to receive the complete request headers from a client before timing out. It applies both when the connection is fully idle and when a request has been started but sending the headers has not been completed. + +By default, the header read timeout is set to 10 seconds. A longer timeout can be configured using the `server.http.header_read_timeout` configuration option. + +```yaml title="router.yaml" +server: + http: + header_read_timeout: 30s +``` + ### Plugins You can customize the router's behavior with [plugins](/router/customizations/overview). Each plugin can have its own section in the configuration file with arbitrary values: @@ -1324,4 +1336,4 @@ You can also view a diff of exactly which changes are necessary to upgrade your ## Related topics -- [Checklist for configuring the router for production](/technotes/TN0008-production-readiness-checklist/#apollo-router) +- [Checklist for configuring the router for production](/technotes/TN0008-production-readiness-checklist/#apollo-router) \ No newline at end of file