Skip to content
13 changes: 13 additions & 0 deletions .changesets/config_header_read_timeout.md
Original file line number Diff line number Diff line change
@@ -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
3 changes: 1 addition & 2 deletions apollo-router/src/axum_factory/axum_http_server_factory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down
12 changes: 12 additions & 0 deletions apollo-router/src/configuration/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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)]
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -291,12 +299,14 @@ impl Configuration {
uplink: Option<UplinkConfig>,
experimental_type_conditioned_fetching: Option<bool>,
batching: Option<Batching>,
server: Option<Server>,
) -> Result<Self, ConfigurationError> {
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(),
Expand Down Expand Up @@ -426,9 +436,11 @@ impl Configuration {
uplink: Option<UplinkConfig>,
batching: Option<Batching>,
experimental_type_conditioned_fetching: Option<bool>,
server: Option<Server>,
) -> Result<Self, ConfigurationError> {
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()),
Expand Down
118 changes: 118 additions & 0 deletions apollo-router/src/configuration/server.rs
Original file line number Diff line number Diff line change
@@ -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<ServerHttpConfig>) -> 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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down Expand Up @@ -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"
Expand Down
30 changes: 30 additions & 0 deletions apollo-router/src/configuration/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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*$"#,
Expand Down
14 changes: 13 additions & 1 deletion docs/source/routing/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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)