[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:
Ahmed Ibrahim
2026-04-22 13:36:34 -07:00
committed by GitHub
parent 18a26d7bbc
commit 9360f267f3
10 changed files with 1169 additions and 105 deletions

View File

@@ -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,
}),
)