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
12 changes: 12 additions & 0 deletions .changesets/fix_caroline_rh_1314.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
### Fix `http2only` not using h2c for cleartext connections ([PR #9018](https://github.com/apollographql/router/pull/9018))

`hyper` does not support [upgrading cleartext connections from HTTP/1.1 to HTTP/2](https://github.com/hyperium/hyper/issues/2411). To use HTTP/2 without TLS, clients must use 'prior knowledge' — connecting with the HTTP/2 preface directly. This is what `experimental_http2: http2only` is for, but previously HTTP/1 was always enabled in the connector, causing the client to fall back to HTTP/1.1 regardless. This fix applies to all outbound HTTP connections: subgraphs, connectors, and coprocessors.

| `experimental_http2` | TLS | protocol used |
|----------------------|-----|-----------------------------------------------|
| `disable` | yes | HTTP/1.1 |
| `disable` | no | HTTP/1.1 |
| `enable` | yes | HTTP/2 (if server supports it), else HTTP/1.1 |
| `enable` | no | HTTP/1.1 |
| `http2only` | yes | HTTP/2 |
| `http2only` | no | HTTP/2 (h2c — cleartext prior knowledge) |
27 changes: 21 additions & 6 deletions apollo-router/src/services/http/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,16 +136,18 @@ impl HttpClientService {

let builder = hyper_rustls::HttpsConnectorBuilder::new()
.with_tls_config(tls_config)
.https_or_http()
.enable_http1();
.https_or_http();

let pool_idle_timeout = client_config.pool_idle_timeout;

let http2 = client_config.experimental_http2.unwrap_or_default();
let connector = if http2 != Http2Config::Disable {
builder.enable_http2().wrap_connector(http_connector)
} else {
builder.wrap_connector(http_connector)
let connector = match http2 {
Http2Config::Enable => builder
.enable_http1()
.enable_http2()
.wrap_connector(http_connector),
Http2Config::Disable => builder.enable_http1().wrap_connector(http_connector),
Http2Config::Http2Only => builder.enable_http2().wrap_connector(http_connector),
};

let http_client =
Expand Down Expand Up @@ -309,6 +311,19 @@ pub(crate) fn generate_tls_client_config(
})
}

#[cfg(test)]
impl HttpClientService {
pub(crate) fn from_client_config(
client_config: crate::configuration::shared::Client,
) -> Result<Self, BoxError> {
// No TLS config - provide empty root store
let tls_root_store = RootCertStore::empty();
let tls_client_config = generate_tls_client_config(tls_root_store, None)?;

HttpClientService::new("test".to_string(), tls_client_config, client_config)
}
}

impl tower::Service<HttpRequest> for HttpClientService {
type Response = HttpResponse;
type Error = BoxError;
Expand Down
80 changes: 56 additions & 24 deletions apollo-router/src/services/http/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -568,45 +568,47 @@ mod tls {

mod h2c_cleartext {
use super::*;
use crate::configuration::shared::Client;

// Starts a local server that responds with a default GraphQL response over plain HTTP.
async fn emulate_h2c_server(listener: TcpListener) {
async fn handle(_request: http::Request<Body>) -> Result<http::Response<Body>, Infallible> {
Ok(http::Response::builder()
.header(CONTENT_TYPE, APPLICATION_JSON.essence_str())
.status(StatusCode::OK)
.body(
serde_json::to_string(&Response {
async fn handle(request: http::Request<Body>) -> Result<http::Response<Body>, Infallible> {
let response_builder =
http::Response::builder().header(CONTENT_TYPE, APPLICATION_JSON.essence_str());

let response = match request.version() {
Version::HTTP_2 => {
let response_body = serde_json::to_string(&Response {
data: Some(Value::default()),
..Response::default()
})
.expect("always valid")
.into(),
)
.unwrap())
});
response_builder
.status(StatusCode::OK)
.body(response_body.unwrap().into())
}
Version::HTTP_11 => response_builder
.status(StatusCode::HTTP_VERSION_NOT_SUPPORTED)
.body(Body::empty()),
version => panic!("unexpected version {version:?}"),
};

Ok(response.unwrap())
}

// XXX(@goto-bus-stop): ideally this server would *only* support HTTP 2 and not HTTP 1
serve(listener, handle).await.unwrap();
}

#[tokio::test(flavor = "multi_thread")]
async fn test_subgraph_h2c() {
async fn test_subgraph_h2c_works_with_http2only() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let socket_addr = listener.local_addr().unwrap();
tokio::task::spawn(emulate_h2c_server(listener));

let subgraph_service = HttpClientService::test_new(
"test",
rustls::ClientConfig::builder()
.with_native_roots()
.expect("read native TLS root certificates")
.with_no_client_auth(),
crate::configuration::shared::Client::builder()
.experimental_http2(Http2Config::Http2Only)
.build(),
)
.expect("can create a HttpService");
let client_config = Client::builder()
.experimental_http2(Http2Config::Http2Only)
.build();
let subgraph_service =
HttpClientService::from_client_config(client_config).expect("can create a HttpService");

let url = Uri::from_str(&format!("http://{socket_addr}")).unwrap();
let response = send_request(
Expand All @@ -615,8 +617,38 @@ mod h2c_cleartext {
r#"{"query":"{ me { name username } }"#,
)
.await;

assert_response_body(response, r#"{"data":null}"#).await;
}

#[tokio::test(flavor = "multi_thread")]
async fn test_subgraph_h2c_not_used_with_enable() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let socket_addr = listener.local_addr().unwrap();
tokio::task::spawn(emulate_h2c_server(listener));

let client_config = Client::builder()
.experimental_http2(Http2Config::Enable)
.build();
let subgraph_service =
HttpClientService::from_client_config(client_config).expect("can create a HttpService");

let url = Uri::from_str(&format!("http://{socket_addr}")).unwrap();
let response = send_request(
subgraph_service,
url,
r#"{"query":"{ me { name username } }"#,
)
.await;

// h2c only works with `Http2Config::Http2Only` - hyper only supports HTTP/2 with TLS or
// with 'prior knowledge'
// https://github.com/hyperium/hyper/issues/2411
assert_eq!(
response.http_response.status(),
StatusCode::HTTP_VERSION_NOT_SUPPORTED
);
}
}

mod compressed_req_res {
Expand Down
Loading