Skip to content

Commit

Permalink
Configuration Options for HTTP/1 Max Headers and Buffer Limits (#6194)
Browse files Browse the repository at this point in the history
Co-authored-by: Jesse Rosenberger <[email protected]>
Co-authored-by: Ivan Goncharov <[email protected]>
Co-authored-by: Simon Sapin <[email protected]>
  • Loading branch information
4 people authored Nov 19, 2024
1 parent c3b8637 commit d2365a9
Show file tree
Hide file tree
Showing 16 changed files with 294 additions and 20 deletions.
19 changes: 19 additions & 0 deletions .changesets/feat_max_headers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
### Configuration Options for HTTP/1 Max Headers and Buffer Limits ([PR #6194](https://github.com/apollographql/router/pull/6194))

This update introduces configuration options that allow you to adjust the maximum number of HTTP/1 request headers and the maximum buffer size allocated for headers.

By default, the Router accepts HTTP/1 requests with up to 100 headers and allocates ~400kib of buffer space to store them. If you need to handle requests with more headers or require a different buffer size, you can now configure these limits in the Router's configuration file:
```yaml
limits:
http1_request_max_headers: 200
http1_request_max_buf_size: 200kib
```
Note for Rust Crate Users: If you are using the Router as a Rust crate, the `http1_request_max_buf_size` option requires the `hyper_header_limits` feature and also necessitates using Apollo's fork of the Hyper crate until the [changes are merged upstream](https://github.com/hyperium/hyper/pull/3523).
You can include this fork by adding the following patch to your Cargo.toml file:
```toml
[patch.crates-io]
"hyper" = { git = "https://github.com/apollographql/hyper.git", tag = "header-customizations-20241108" }
```

By [@IvanGoncharov](https://github.com/IvanGoncharov) in https://github.com/apollographql/router/pull/6194
8 changes: 4 additions & 4 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -507,7 +507,7 @@ commands:
# TODO: remove this workaround once we update to Xcode >= 15.1.0
# See: https://github.com/apollographql/router/pull/5462
RUST_LIB_BACKTRACE: 0
command: xtask test --workspace --locked --features ci
command: xtask test --workspace --locked --features ci,hyper_header_limits
- run:
name: Delete large files from cache
command: |
Expand Down Expand Up @@ -655,10 +655,10 @@ jobs:
- run: cargo xtask release prepare nightly
- run:
command: >
cargo xtask dist --target aarch64-apple-darwin
cargo xtask dist --target aarch64-apple-darwin --features hyper_header_limits
- run:
command: >
cargo xtask dist --target x86_64-apple-darwin
cargo xtask dist --target x86_64-apple-darwin --features hyper_header_limits
- run:
command: >
mkdir -p artifacts
Expand Down Expand Up @@ -718,7 +718,7 @@ jobs:
- run: cargo xtask release prepare nightly
- run:
command: >
cargo xtask dist
cargo xtask dist --features hyper_header_limits
- run:
command: >
mkdir -p artifacts
Expand Down
6 changes: 3 additions & 3 deletions Cargo.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3382,9 +3382,8 @@ dependencies = [

[[package]]
name = "hyper"
version = "0.14.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a152ddd61dfaec7273fe8419ab357f33aee0d914c5f4efbf0d96fa749eea5ec9"
version = "0.14.31"
source = "git+https://github.com/apollographql/hyper.git?tag=header-customizations-20241108#c42aec785394b40645a283384838b856beace011"
dependencies = [
"bytes",
"futures-channel",
Expand All @@ -3397,6 +3396,7 @@ dependencies = [
"httpdate",
"itoa",
"pin-project-lite",
"smallvec",
"socket2 0.5.7",
"tokio",
"tower-service",
Expand Down
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,6 @@ sha1 = "0.10.6"
tempfile = "3.10.1"
tokio = { version = "1.36.0", features = ["full"] }
tower = { version = "0.4.13", features = ["full"] }

[patch.crates-io]
"hyper" = { git = "https://github.com/apollographql/hyper.git", tag = "header-customizations-20241108" }
6 changes: 5 additions & 1 deletion apollo-router/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ docs_rs = ["router-bridge/docs_rs"]
# and not yet ready for production use.
telemetry_next = []

# Allow Router to use feature from custom fork of Hyper until it is merged:
# https://github.com/hyperium/hyper/pull/3523
hyper_header_limits = []

# is set when ci builds take place. It allows us to disable some tests when CI is running on certain platforms.
ci = []

Expand Down Expand Up @@ -105,7 +109,7 @@ http-body = "0.4.6"
heck = "0.5.0"
humantime = "2.1.0"
humantime-serde = "1.1.1"
hyper = { version = "0.14.28", features = ["server", "client", "stream"] }
hyper = { version = "0.14.31", features = ["server", "client", "stream"] }
hyper-rustls = { version = "0.24.2", features = ["http1", "http2"] }
indexmap = { version = "2.2.6", features = ["serde"] }
itertools = "0.13.0"
Expand Down
16 changes: 16 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 @@ -5,6 +5,7 @@ use std::sync::atomic::AtomicBool;
use std::sync::atomic::AtomicU64;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::time::Duration;
use std::time::Instant;

use axum::error_handling::HandleErrorLayer;
Expand All @@ -24,6 +25,7 @@ use http::header::CONTENT_ENCODING;
use http::HeaderValue;
use http::Request;
use http_body::combinators::UnsyncBoxBody;
use hyper::server::conn::Http;
use hyper::Body;
use itertools::Itertools;
use multimap::MultiMap;
Expand Down Expand Up @@ -298,12 +300,25 @@ impl HttpServerFactory for AxumHttpServerFactory {
let actual_main_listen_address = main_listener
.local_addr()
.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));

#[cfg(feature = "hyper_header_limits")]
if let Some(max_headers) = configuration.limits.http1_max_request_headers {
http_config.http1_max_headers(max_headers);
}

if let Some(max_buf_size) = configuration.limits.http1_max_request_buf_size {
http_config.max_buf_size(max_buf_size.as_u64() as usize);
}

let (main_server, main_shutdown_sender) = serve_router_on_listen_addr(
main_listener,
actual_main_listen_address.clone(),
all_routers.main.1,
true,
http_config.clone(),
all_connections_stopped_sender.clone(),
);

Expand Down Expand Up @@ -343,6 +358,7 @@ impl HttpServerFactory for AxumHttpServerFactory {
listen_addr.clone(),
router,
false,
http_config.clone(),
all_connections_stopped_sender.clone(),
);
(
Expand Down
15 changes: 5 additions & 10 deletions apollo-router/src/axum_factory/listeners.rs
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ pub(super) fn serve_router_on_listen_addr(
address: ListenAddr,
router: axum::Router,
main_graphql_port: bool,
http_config: Http,
all_connections_stopped_sender: mpsc::Sender<()>,
) -> (impl Future<Output = Listener>, oneshot::Sender<()>) {
let (shutdown_sender, shutdown_receiver) = oneshot::channel::<()>();
Expand Down Expand Up @@ -243,6 +244,7 @@ pub(super) fn serve_router_on_listen_addr(
}

let address = address.clone();
let mut http_config = http_config.clone();
tokio::task::spawn(async move {
// this sender must be moved into the session to track that it is still running
let _connection_stop_signal = connection_stop_signal;
Expand All @@ -261,11 +263,8 @@ pub(super) fn serve_router_on_listen_addr(
.expect(
"this should not fail unless the socket is invalid",
);
let connection = Http::new()
.http1_keep_alive(true)
.http1_header_read_timeout(Duration::from_secs(10))
.serve_connection(stream, app);

let connection = http_config.serve_connection(stream, app);
tokio::pin!(connection);
tokio::select! {
// the connection finished first
Expand All @@ -291,9 +290,7 @@ pub(super) fn serve_router_on_listen_addr(
NetworkStream::Unix(stream) => {
let received_first_request = Arc::new(AtomicBool::new(false));
let app = IdleConnectionChecker::new(received_first_request.clone(), app);
let connection = Http::new()
.http1_keep_alive(true)
.serve_connection(stream, app);
let connection = http_config.serve_connection(stream, app);

tokio::pin!(connection);
tokio::select! {
Expand Down Expand Up @@ -329,9 +326,7 @@ pub(super) fn serve_router_on_listen_addr(
let protocol = stream.get_ref().1.alpn_protocol();
let http2 = protocol == Some(&b"h2"[..]);

let connection = Http::new()
.http1_keep_alive(true)
.http1_header_read_timeout(Duration::from_secs(10))
let connection = http_config
.http2_only(http2)
.serve_connection(stream, app);

Expand Down
8 changes: 8 additions & 0 deletions apollo-router/src/configuration/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,14 @@ impl Configuration {

impl Configuration {
pub(crate) fn validate(self) -> Result<Self, ConfigurationError> {
#[cfg(not(feature = "hyper_header_limits"))]
if self.limits.http1_max_request_headers.is_some() {
return Err(ConfigurationError::InvalidConfiguration {
message: "'limits.http1_max_request_headers' requires 'hyper_header_limits' feature",
error: "enable 'hyper_header_limits' feature in order to use 'limits.http1_max_request_headers'".to_string(),
});
}

// Sandbox and Homepage cannot be both enabled
if self.sandbox.enabled && self.homepage.enabled {
return Err(ConfigurationError::InvalidConfiguration {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
---
source: apollo-router/src/configuration/tests.rs
expression: "&schema"
snapshot_kind: text
---
{
"$schema": "http://json-schema.org/draft-07/schema#",
Expand Down Expand Up @@ -1317,6 +1318,20 @@ expression: "&schema"
"additionalProperties": false,
"description": "Configuration for operation limits, parser limits, HTTP limits, etc.",
"properties": {
"http1_max_request_buf_size": {
"default": null,
"description": "Limit the maximum buffer size for the HTTP1 connection.\n\nDefault is ~400kib.",
"nullable": true,
"type": "string"
},
"http1_max_request_headers": {
"default": null,
"description": "Limit the maximum number of headers of incoming HTTP1 requests. Default is 100.\n\nIf router receives more headers than the buffer size, it responds to the client with \"431 Request Header Fields Too Large\".",
"format": "uint",
"minimum": 0.0,
"nullable": true,
"type": "integer"
},
"http_max_request_bytes": {
"default": 2000000,
"description": "Limit the size of incoming HTTP requests read from the network, to protect against running out of memory. Default: 2000000 (2 MB)",
Expand Down
5 changes: 5 additions & 0 deletions apollo-router/src/configuration/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,11 @@ fn validate_project_config_files() {
};

for yaml in yamls {
#[cfg(not(feature = "hyper_header_limits"))]
if yaml.contains("http1_max_request_headers") {
continue;
}

if let Err(e) = validate_yaml_configuration(
&yaml,
Expansion::default().unwrap(),
Expand Down
16 changes: 16 additions & 0 deletions apollo-router/src/plugins/limits/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ mod limited;
use std::error::Error;

use async_trait::async_trait;
use bytesize::ByteSize;
use http::StatusCode;
use schemars::JsonSchema;
use serde::Deserialize;
Expand Down Expand Up @@ -101,6 +102,19 @@ pub(crate) struct Config {
/// Limit the size of incoming HTTP requests read from the network,
/// to protect against running out of memory. Default: 2000000 (2 MB)
pub(crate) http_max_request_bytes: usize,

/// Limit the maximum number of headers of incoming HTTP1 requests. Default is 100.
///
/// If router receives more headers than the buffer size, it responds to the client with
/// "431 Request Header Fields Too Large".
///
pub(crate) http1_max_request_headers: Option<usize>,

/// Limit the maximum buffer size for the HTTP1 connection.
///
/// Default is ~400kib.
#[schemars(with = "Option<String>", default)]
pub(crate) http1_max_request_buf_size: Option<ByteSize>,
}

impl Default for Config {
Expand All @@ -113,6 +127,8 @@ impl Default for Config {
max_aliases: None,
warn_only: false,
http_max_request_bytes: 2_000_000,
http1_max_request_headers: None,
http1_max_request_buf_size: None,
parser_max_tokens: 15_000,

// This is `apollo-parser`’s default, which protects against stack overflow
Expand Down
1 change: 1 addition & 0 deletions apollo-router/tests/integration/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ mod operation_limits;
mod operation_name;
mod query_planner;
mod subgraph_response;
mod supergraph;
mod traffic_shaping;
mod typename;

Expand Down
Loading

0 comments on commit d2365a9

Please sign in to comment.