Add a health endpoint to the code-mode gRPC listener (#38806)

## What changed

- Serve `GET /healthz` with a `200 OK` response over HTTP/1.1 and HTTP/2.
- Continue requiring HTTP/2 for all other requests so gRPC methods are not exposed over HTTP/1.1.

## Testing

- Add TCP listener integration coverage for HTTP/1.1 and HTTP/2 health checks and rejection of HTTP/1.1 gRPC requests.

GitOrigin-RevId: ae7bbf56323fbc76769375a6d8e90653e8adc860
This commit is contained in:
Channing Conger authored and copyberry committed 2026-08-15 21:26:28 +00:00
1 parent 899d1715c8
commit b3cc217378
2 files changed
+133 -5

No files matched your search

+104
View File
@@ -1,18 +1,122 @@
use std::future::poll_fn;
use std::process::Stdio;
use std::time::Duration;
use anyhow::Context;
use anyhow::Result;
use axum::http::Request;
use axum::http::StatusCode;
use axum::http::Version;
use codex_code_mode_protocol::grpc;
use codex_code_mode_protocol::grpc::code_mode_host_client::CodeModeHostClient;
use pretty_assertions::assert_eq;
use tokio::io::AsyncBufReadExt;
use tokio::io::AsyncReadExt;
use tokio::io::AsyncWriteExt;
use tokio::io::BufReader;
use tokio::net::TcpStream;
use tokio::process::Command;
use tokio::time::timeout;
use tonic::body::Body;
use tonic::codegen::Service;
use tonic::transport::Endpoint;
#[path = "support/host.rs"]
mod host;
use host::HostHarness;
const TEST_TIMEOUT: Duration = Duration::from_secs(/*secs*/ 10);
#[tokio::test]
async fn tcp_listener_serves_http1_healthz() -> Result<()> {
let host = HostHarness::start("grpc://127.0.0.1:0").await?;
let request = "GET /healthz HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n";
let response = http1_response(&host.endpoint, request.as_bytes()).await?;
assert_eq!(response.lines().next(), Some("HTTP/1.1 200 OK"));
Ok(())
}
#[tokio::test]
async fn tcp_listener_rejects_http1_grpc_requests() -> Result<()> {
let host = HostHarness::start("grpc://127.0.0.1:0").await?;
let mut request = concat!(
"POST /codex.code_mode.v1.CodeModeHost/OpenSession HTTP/1.1\r\n",
"Host: localhost\r\n",
"Content-Type: application/grpc\r\n",
"Content-Length: 5\r\n",
"Connection: close\r\n\r\n"
)
.as_bytes()
.to_vec();
request.extend_from_slice(&[0; 5]);
let response = http1_response(&host.endpoint, &request).await?;
assert_eq!(
response.lines().next(),
Some("HTTP/1.1 505 HTTP Version Not Supported")
);
Ok(())
}
#[tokio::test]
async fn tcp_listener_serves_http2_healthz() -> Result<()> {
let host = HostHarness::start("grpc://127.0.0.1:0").await?;
let mut channel = Endpoint::from_shared(host.endpoint.clone())
.context("gRPC code-mode host published an invalid endpoint")?
.connect_timeout(TEST_TIMEOUT)
.timeout(TEST_TIMEOUT)
.connect()
.await
.context("failed to connect to gRPC code-mode host health endpoint")?;
let request = Request::builder()
.uri(format!("{}/healthz", host.endpoint))
.body(Body::empty())
.context("failed to build gRPC code-mode host health request")?;
let response = timeout(TEST_TIMEOUT, async {
poll_fn(|context| channel.poll_ready(context))
.await
.context("gRPC code-mode host health channel is unavailable")?;
channel
.call(request)
.await
.context("failed to request gRPC code-mode host health")
})
.await
.context("timed out requesting gRPC code-mode host health")??;
assert_eq!(response.version(), Version::HTTP_2);
assert_eq!(response.status(), StatusCode::OK);
Ok(())
}
async fn http1_response(endpoint: &str, request: &[u8]) -> Result<String> {
let address = endpoint
.strip_prefix("http://")
.context("gRPC code-mode host URL should use http://")?;
timeout(TEST_TIMEOUT, async {
let mut stream = TcpStream::connect(address)
.await
.context("failed to connect to gRPC code-mode host")?;
stream
.write_all(request)
.await
.context("failed to send HTTP/1.1 request to gRPC code-mode host")?;
let mut response = String::new();
stream
.read_to_string(&mut response)
.await
.context("failed to read HTTP/1.1 response from gRPC code-mode host")?;
Ok(response)
})
.await
.context("timed out waiting for HTTP/1.1 response from gRPC code-mode host")?
}
#[tokio::test]
async fn tcp_listener_opens_a_grpc_session() -> Result<()> {
let mut host = Command::new(codex_utils_cargo_bin::cargo_bin("codex-code-mode-host")?)