Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
2 changes: 2 additions & 0 deletions apollo-router/src/axum_factory/axum_http_server_factory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ impl HttpServerFactory for AxumHttpServerFactory {
all_routers.main.1,
configuration.limits.http1_max_request_headers,
configuration.limits.http1_max_request_buf_size,
configuration.server.http.header_read_timeout,
all_connections_stopped_sender.clone(),
);

Expand Down Expand Up @@ -268,6 +269,7 @@ impl HttpServerFactory for AxumHttpServerFactory {
router,
configuration.limits.http1_max_request_headers,
configuration.limits.http1_max_request_buf_size,
configuration.server.http.header_read_timeout,
all_connections_stopped_sender.clone(),
);
(
Expand Down
7 changes: 4 additions & 3 deletions apollo-router/src/axum_factory/listeners.rs
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,7 @@ pub(super) fn serve_router_on_listen_addr(
router: axum::Router,
opt_max_headers: Option<usize>,
opt_max_buf_size: Option<ByteSize>,
header_read_timeout: Duration,
all_connections_stopped_sender: mpsc::Sender<()>,
) -> (impl Future<Output = Listener>, oneshot::Sender<()>) {
let (shutdown_sender, shutdown_receiver) = oneshot::channel::<()>();
Expand Down Expand Up @@ -381,7 +382,7 @@ pub(super) fn serve_router_on_listen_addr(
let http_config = http_connection
.keep_alive(true)
.timer(TokioTimer::new())
.header_read_timeout(Duration::from_secs(10));
.header_read_timeout(header_read_timeout);
if let Some(max_headers) = opt_max_headers {
http_config.max_headers(max_headers);
}
Expand All @@ -405,7 +406,7 @@ pub(super) fn serve_router_on_listen_addr(
let http_config = http_connection
.keep_alive(true)
.timer(TokioTimer::new())
.header_read_timeout(Duration::from_secs(10));
.header_read_timeout(header_read_timeout);
if let Some(max_headers) = opt_max_headers {
http_config.max_headers(max_headers);
}
Expand Down Expand Up @@ -439,7 +440,7 @@ pub(super) fn serve_router_on_listen_addr(
let http_config = http_connection
.keep_alive(true)
.timer(TokioTimer::new())
.header_read_timeout(Duration::from_secs(10));
.header_read_timeout(header_read_timeout);
if let Some(max_headers) = opt_max_headers {
http_config.max_headers(max_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 @@ pub(crate) use self::experimental::Discussed;
pub(crate) use self::schema::generate_config_schema;
pub(crate) use self::schema::generate_upgrade;
pub(crate) use self::schema::validate_yaml_configuration;
use self::server::Server;
use self::subgraph::SubgraphConfiguration;
use crate::ApolloRouterError;
use crate::cache::DEFAULT_CACHE_CAPACITY;
Expand All @@ -67,6 +68,7 @@ mod experimental;
pub(crate) mod metrics;
mod persisted_queries;
pub(crate) mod schema;
pub(crate) mod server;
pub(crate) mod shared;
pub(crate) mod subgraph;
#[cfg(test)]
Expand Down Expand Up @@ -155,6 +157,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 @@ -227,6 +233,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 @@ -261,6 +268,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 @@ -309,12 +317,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 @@ -444,9 +454,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::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 @@ -5816,6 +5816,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"
},
"Source": {
"oneOf": [
{
Expand Down Expand Up @@ -9008,6 +9033,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 @@ -1169,6 +1169,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 @@ -1224,6 +1224,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 @@ -1296,4 +1308,4 @@ Here, the `name` and `value` entries under `&insert_custom_header` are reused un

## 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)