codex-rs/app-server: add health endpoints for --listen websocket server (#13782)

Healthcheck endpoints for the websocket server

- serve `GET /readyz` and `GET /healthz` from the same listener used for
`--listen ws://...`
- switch the websocket listener over to `axum` upgrade handling instead
of manual socket parsing
- add websocket transport coverage for the health endpoints and document
the new behavior

Testing
- integration tests
- built and tested e2e

```
> curl -i http://127.0.0.1:9234/readyz
HTTP/1.1 200 OK
content-length: 0
date: Fri, 06 Mar 2026 19:20:23 GMT

>  curl -i http://127.0.0.1:9234/healthz
HTTP/1.1 200 OK
content-length: 0
date: Fri, 06 Mar 2026 19:20:24 GMT
```
This commit is contained in:
Max Johnson
2026-03-09 15:11:30 -07:00
committed by GitHub
parent d309c102ef
commit 66e71cce11
5 changed files with 130 additions and 52 deletions

View File

@@ -12,6 +12,7 @@ use codex_app_server_protocol::JSONRPCResponse;
use codex_app_server_protocol::RequestId;
use futures::SinkExt;
use futures::StreamExt;
use reqwest::StatusCode;
use serde_json::json;
use std::net::SocketAddr;
use std::path::Path;
@@ -79,6 +80,34 @@ async fn websocket_transport_routes_per_connection_handshake_and_responses() ->
Ok(())
}
#[tokio::test]
async fn websocket_transport_serves_health_endpoints_on_same_listener() -> Result<()> {
let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await;
let codex_home = TempDir::new()?;
create_config_toml(codex_home.path(), &server.uri(), "never")?;
let bind_addr = reserve_local_addr()?;
let mut process = spawn_websocket_server(codex_home.path(), bind_addr).await?;
let client = reqwest::Client::new();
let readyz = http_get(&client, bind_addr, "/readyz").await?;
assert_eq!(readyz.status(), StatusCode::OK);
let healthz = http_get(&client, bind_addr, "/healthz").await?;
assert_eq!(healthz.status(), StatusCode::OK);
let mut ws = connect_websocket(bind_addr).await?;
send_initialize_request(&mut ws, 1, "ws_health_client").await?;
let init = read_response_for_id(&mut ws, 1).await?;
assert_eq!(init.id, RequestId::Integer(1));
process
.kill()
.await
.context("failed to stop websocket app-server process")?;
Ok(())
}
pub(super) async fn spawn_websocket_server(
codex_home: &Path,
bind_addr: SocketAddr,
@@ -133,6 +162,30 @@ pub(super) async fn connect_websocket(bind_addr: SocketAddr) -> Result<WsClient>
}
}
async fn http_get(
client: &reqwest::Client,
bind_addr: SocketAddr,
path: &str,
) -> Result<reqwest::Response> {
let deadline = Instant::now() + Duration::from_secs(10);
loop {
match client
.get(format!("http://{bind_addr}{path}"))
.send()
.await
.with_context(|| format!("failed to GET http://{bind_addr}{path}"))
{
Ok(response) => return Ok(response),
Err(err) => {
if Instant::now() >= deadline {
bail!("failed to GET http://{bind_addr}{path}: {err}");
}
sleep(Duration::from_millis(50)).await;
}
}
}
}
pub(super) async fn send_initialize_request(
stream: &mut WsClient,
id: i64,