mirror of
https://github.com/openai/codex.git
synced 2026-05-01 18:06:47 +00:00
[2/4] Implement executor HTTP request runner (#18582)
### Why Remote streamable HTTP MCP needs the executor to perform ordinary HTTP requests on the executor side. This keeps network placement aligned with `experimental_environment = "remote"` without adding MCP-specific executor APIs. ### What - Add an executor-side `http/request` runner backed by `reqwest`. - Validate request method and URL scheme, preserving the transport boundary at plain HTTP. - Return buffered responses for ordinary calls and emit ordered `http/request/bodyDelta` notifications for streaming responses. - Register the request handler in the exec-server router. - Document the runner entrypoint, conversion helpers, body-stream bridge, notification sender, timeout behavior, and new integration-test helpers. - Add exec-server integration tests with the existing websocket harness and a local TCP HTTP peer for buffered and streamed responses, with comments spelling out what each test proves and its setup/exercise/assert phases. ### Stack 1. #18581 protocol 2. #18582 runner 3. #18583 RMCP client 4. #18584 manager wiring and local/remote coverage ### Verification - `just fmt` - `cargo check -p codex-exec-server -p codex-rmcp-client --tests` - `cargo check -p codex-core --test all` compile-only - `git diff --check` - Online full CI is running from the `full-ci` branch, including the remote Rust test job. Co-authored-by: Codex <noreply@openai.com> --------- Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -42,6 +42,7 @@ const HTTP_REQUEST_BODY_DELTA_METHOD: &str = "http/request/bodyDelta";
|
||||
const INITIALIZE_METHOD: &str = "initialize";
|
||||
const INITIALIZED_METHOD: &str = "initialized";
|
||||
const TEST_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
const HTTP_BODY_DELTA_CHANNEL_CAPACITY: u64 = 256;
|
||||
const OVERFLOWING_BODY_DELTA_FRAMES: u64 = 1_024;
|
||||
|
||||
/// What this tests: the buffered HTTP helper always sends a buffered
|
||||
@@ -51,8 +52,8 @@ async fn http_request_forces_buffered_request_params() -> Result<()> {
|
||||
// Phase 1: start a fake WebSocket exec-server so the test covers the
|
||||
// public client connection path without depending on the HTTP runner.
|
||||
let server = spawn_scripted_exec_server(|mut peer| async move {
|
||||
// Phase 2: verify the buffered helper strips streaming-only fields
|
||||
// before it sends the JSON-RPC call.
|
||||
// Phase 2: verify the buffered helper forces buffered mode before it
|
||||
// sends the JSON-RPC call.
|
||||
let (request_id, params) = peer.read_http_request().await?;
|
||||
assert_eq!(
|
||||
params,
|
||||
@@ -62,7 +63,7 @@ async fn http_request_forces_buffered_request_params() -> Result<()> {
|
||||
headers: Vec::new(),
|
||||
body: None,
|
||||
timeout_ms: None,
|
||||
request_id: None,
|
||||
request_id: "ignored-stream-id".to_string(),
|
||||
stream_response: false,
|
||||
}
|
||||
);
|
||||
@@ -90,7 +91,7 @@ async fn http_request_forces_buffered_request_params() -> Result<()> {
|
||||
headers: Vec::new(),
|
||||
body: None,
|
||||
timeout_ms: None,
|
||||
request_id: Some("ignored-stream-id".to_string()),
|
||||
request_id: "ignored-stream-id".to_string(),
|
||||
stream_response: true,
|
||||
}),
|
||||
)
|
||||
@@ -130,7 +131,7 @@ async fn http_response_body_stream_uses_generated_ids_and_receives_ordered_delta
|
||||
}],
|
||||
body: None,
|
||||
timeout_ms: None,
|
||||
request_id: Some("http-1".to_string()),
|
||||
request_id: "http-1".to_string(),
|
||||
stream_response: true,
|
||||
}
|
||||
);
|
||||
@@ -185,7 +186,7 @@ async fn http_response_body_stream_uses_generated_ids_and_receives_ordered_delta
|
||||
headers: Vec::new(),
|
||||
body: None,
|
||||
timeout_ms: None,
|
||||
request_id: Some("http-2".to_string()),
|
||||
request_id: "http-2".to_string(),
|
||||
stream_response: true,
|
||||
}
|
||||
);
|
||||
@@ -214,7 +215,7 @@ async fn http_response_body_stream_uses_generated_ids_and_receives_ordered_delta
|
||||
}],
|
||||
body: None,
|
||||
timeout_ms: None,
|
||||
request_id: Some("caller-stream-id".to_string()),
|
||||
request_id: "caller-stream-id".to_string(),
|
||||
stream_response: false,
|
||||
}),
|
||||
)
|
||||
@@ -252,7 +253,7 @@ async fn http_response_body_stream_uses_generated_ids_and_receives_ordered_delta
|
||||
headers: Vec::new(),
|
||||
body: None,
|
||||
timeout_ms: None,
|
||||
request_id: Some("caller-stream-id".to_string()),
|
||||
request_id: "caller-stream-id".to_string(),
|
||||
stream_response: false,
|
||||
}),
|
||||
)
|
||||
@@ -288,7 +289,7 @@ async fn http_response_body_stream_drops_queued_terminal_before_next_generated_i
|
||||
headers: Vec::new(),
|
||||
body: None,
|
||||
timeout_ms: None,
|
||||
request_id: Some("http-1".to_string()),
|
||||
request_id: "http-1".to_string(),
|
||||
stream_response: true,
|
||||
}
|
||||
);
|
||||
@@ -321,7 +322,7 @@ async fn http_response_body_stream_drops_queued_terminal_before_next_generated_i
|
||||
headers: Vec::new(),
|
||||
body: None,
|
||||
timeout_ms: None,
|
||||
request_id: Some("http-2".to_string()),
|
||||
request_id: "http-2".to_string(),
|
||||
stream_response: true,
|
||||
}
|
||||
);
|
||||
@@ -347,7 +348,7 @@ async fn http_response_body_stream_drops_queued_terminal_before_next_generated_i
|
||||
headers: Vec::new(),
|
||||
body: None,
|
||||
timeout_ms: None,
|
||||
request_id: Some("caller-stream-id".to_string()),
|
||||
request_id: "caller-stream-id".to_string(),
|
||||
stream_response: false,
|
||||
}),
|
||||
)
|
||||
@@ -371,7 +372,7 @@ async fn http_response_body_stream_drops_queued_terminal_before_next_generated_i
|
||||
headers: Vec::new(),
|
||||
body: None,
|
||||
timeout_ms: None,
|
||||
request_id: Some("caller-stream-id".to_string()),
|
||||
request_id: "caller-stream-id".to_string(),
|
||||
stream_response: false,
|
||||
};
|
||||
let (reuse_response, _reuse_body_stream) =
|
||||
@@ -410,7 +411,7 @@ async fn http_response_body_stream_ignores_late_deltas_after_cancelled_request()
|
||||
headers: Vec::new(),
|
||||
body: None,
|
||||
timeout_ms: None,
|
||||
request_id: Some("http-1".to_string()),
|
||||
request_id: "http-1".to_string(),
|
||||
stream_response: true,
|
||||
}
|
||||
);
|
||||
@@ -429,7 +430,7 @@ async fn http_response_body_stream_ignores_late_deltas_after_cancelled_request()
|
||||
headers: Vec::new(),
|
||||
body: None,
|
||||
timeout_ms: None,
|
||||
request_id: Some("http-2".to_string()),
|
||||
request_id: "http-2".to_string(),
|
||||
stream_response: true,
|
||||
}
|
||||
);
|
||||
@@ -473,7 +474,7 @@ async fn http_response_body_stream_ignores_late_deltas_after_cancelled_request()
|
||||
headers: Vec::new(),
|
||||
body: None,
|
||||
timeout_ms: None,
|
||||
request_id: Some("caller-stream-id".to_string()),
|
||||
request_id: "caller-stream-id".to_string(),
|
||||
stream_response: false,
|
||||
})
|
||||
.await;
|
||||
@@ -494,7 +495,7 @@ async fn http_response_body_stream_ignores_late_deltas_after_cancelled_request()
|
||||
headers: Vec::new(),
|
||||
body: None,
|
||||
timeout_ms: None,
|
||||
request_id: Some("caller-stream-id".to_string()),
|
||||
request_id: "caller-stream-id".to_string(),
|
||||
stream_response: false,
|
||||
}),
|
||||
)
|
||||
@@ -540,7 +541,7 @@ async fn http_response_body_stream_ignores_late_deltas_after_drop() -> Result<()
|
||||
headers: Vec::new(),
|
||||
body: None,
|
||||
timeout_ms: None,
|
||||
request_id: Some("http-1".to_string()),
|
||||
request_id: "http-1".to_string(),
|
||||
stream_response: true,
|
||||
}
|
||||
);
|
||||
@@ -579,7 +580,7 @@ async fn http_response_body_stream_ignores_late_deltas_after_drop() -> Result<()
|
||||
headers: Vec::new(),
|
||||
body: None,
|
||||
timeout_ms: None,
|
||||
request_id: Some("http-2".to_string()),
|
||||
request_id: "http-2".to_string(),
|
||||
stream_response: true,
|
||||
}
|
||||
);
|
||||
@@ -614,7 +615,7 @@ async fn http_response_body_stream_ignores_late_deltas_after_drop() -> Result<()
|
||||
headers: Vec::new(),
|
||||
body: None,
|
||||
timeout_ms: None,
|
||||
request_id: Some("caller-stream-id".to_string()),
|
||||
request_id: "caller-stream-id".to_string(),
|
||||
stream_response: false,
|
||||
}),
|
||||
)
|
||||
@@ -646,7 +647,7 @@ async fn http_response_body_stream_ignores_late_deltas_after_drop() -> Result<()
|
||||
headers: Vec::new(),
|
||||
body: None,
|
||||
timeout_ms: None,
|
||||
request_id: Some("caller-stream-id".to_string()),
|
||||
request_id: "caller-stream-id".to_string(),
|
||||
stream_response: false,
|
||||
}),
|
||||
)
|
||||
@@ -690,7 +691,7 @@ async fn http_response_body_stream_fails_when_transport_disconnects() -> Result<
|
||||
headers: Vec::new(),
|
||||
body: None,
|
||||
timeout_ms: None,
|
||||
request_id: Some("http-1".to_string()),
|
||||
request_id: "http-1".to_string(),
|
||||
stream_response: true,
|
||||
}
|
||||
);
|
||||
@@ -716,7 +717,7 @@ async fn http_response_body_stream_fails_when_transport_disconnects() -> Result<
|
||||
headers: Vec::new(),
|
||||
body: None,
|
||||
timeout_ms: None,
|
||||
request_id: Some("caller-stream-id".to_string()),
|
||||
request_id: "caller-stream-id".to_string(),
|
||||
stream_response: false,
|
||||
}),
|
||||
)
|
||||
@@ -742,6 +743,98 @@ async fn http_response_body_stream_fails_when_transport_disconnects() -> Result<
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// What this tests: transport disconnect still records a terminal stream
|
||||
/// failure even when the client-side body-delta queue is already full.
|
||||
#[tokio::test]
|
||||
async fn http_response_body_stream_reports_disconnect_when_queue_is_full() -> Result<()> {
|
||||
// Phase 1: fill the queued body-delta route exactly to capacity before the
|
||||
// response headers arrive, then drop the transport without sending EOF.
|
||||
let server = spawn_scripted_exec_server(|mut peer| async move {
|
||||
let (request_id, params) = peer.read_http_request().await?;
|
||||
assert_eq!(
|
||||
params,
|
||||
HttpRequestParams {
|
||||
method: "GET".to_string(),
|
||||
url: "https://example.test/mcp/disconnect-full-queue".to_string(),
|
||||
headers: Vec::new(),
|
||||
body: None,
|
||||
timeout_ms: None,
|
||||
request_id: "http-1".to_string(),
|
||||
stream_response: true,
|
||||
}
|
||||
);
|
||||
for seq in 1..=HTTP_BODY_DELTA_CHANNEL_CAPACITY {
|
||||
peer.write_body_delta(HttpRequestBodyDeltaNotification {
|
||||
request_id: "http-1".to_string(),
|
||||
seq,
|
||||
delta: b"x".to_vec().into(),
|
||||
done: false,
|
||||
error: None,
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
peer.write_response(
|
||||
request_id,
|
||||
HttpRequestResponse {
|
||||
status: 200,
|
||||
headers: Vec::new(),
|
||||
body: Vec::new().into(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
})
|
||||
.await?;
|
||||
let client = server.connect_client().await?;
|
||||
|
||||
// Phase 2: start the streaming request and receive headers while the
|
||||
// queue is already full.
|
||||
let (_response, mut body_stream) = timeout(
|
||||
TEST_TIMEOUT,
|
||||
client.http_request_stream(HttpRequestParams {
|
||||
method: "GET".to_string(),
|
||||
url: "https://example.test/mcp/disconnect-full-queue".to_string(),
|
||||
headers: Vec::new(),
|
||||
body: None,
|
||||
timeout_ms: None,
|
||||
request_id: "caller-stream-id".to_string(),
|
||||
stream_response: false,
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.context("streamed http/request should return headers")??;
|
||||
|
||||
// Phase 3: drain the queued chunks and assert the transport disconnect is
|
||||
// still reported as an error rather than a clean EOF.
|
||||
let mut chunks = 0;
|
||||
let error = loop {
|
||||
match timeout(TEST_TIMEOUT, body_stream.recv())
|
||||
.await
|
||||
.context("disconnect should wake the full queued body stream")?
|
||||
{
|
||||
Ok(Some(_chunk)) => {
|
||||
chunks += 1;
|
||||
}
|
||||
Ok(None) => bail!("disconnect with a full queue should not look like clean EOF"),
|
||||
Err(error) => break error,
|
||||
}
|
||||
};
|
||||
assert_eq!(
|
||||
(
|
||||
chunks,
|
||||
error
|
||||
.to_string()
|
||||
.starts_with(
|
||||
"exec-server protocol error: http response stream `http-1` failed: exec-server transport disconnected",
|
||||
),
|
||||
),
|
||||
(HTTP_BODY_DELTA_CHANNEL_CAPACITY as usize, true)
|
||||
);
|
||||
|
||||
drop(client);
|
||||
server.finish().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// What this tests: body-delta backpressure closes the public body stream as
|
||||
/// an error rather than letting callers accept a truncated body as clean EOF.
|
||||
#[tokio::test]
|
||||
@@ -759,7 +852,7 @@ async fn http_response_body_stream_reports_backpressure_truncation() -> Result<(
|
||||
headers: Vec::new(),
|
||||
body: None,
|
||||
timeout_ms: None,
|
||||
request_id: Some("http-1".to_string()),
|
||||
request_id: "http-1".to_string(),
|
||||
stream_response: true,
|
||||
}
|
||||
);
|
||||
@@ -801,7 +894,7 @@ async fn http_response_body_stream_reports_backpressure_truncation() -> Result<(
|
||||
headers: Vec::new(),
|
||||
body: None,
|
||||
timeout_ms: None,
|
||||
request_id: Some("caller-stream-id".to_string()),
|
||||
request_id: "caller-stream-id".to_string(),
|
||||
stream_response: false,
|
||||
}),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user