mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
[codex] Retry streamable HTTP initialize failures (#25147)
## Summary - Retry transient streamable HTTP failures during RMCP startup when the failure happens while sending the initialize request. - Retry transient streamable HTTP failures for tools/list, which is read-only and safe to replay. - Cover both retryable HTTP statuses and request-layer failures where no HTTP status is returned. - Surface retryable HTTP statuses from the streamable HTTP adapter as typed client errors. - Add integration coverage for initialize retry, tools/list retry, no-status request failure retry, and non-retryable initialize status. ## Root cause The observed codex_apps failures can happen before normal tool execution: RMCP startup fails while sending initialize, or the first read-only tools/list fails after startup. Retrying hosted_apps_bridge tools/call would not cover initialize and would risk replaying side-effecting tool calls. This change retries the streamable HTTP handshake itself, recreates the transport between initialize attempts, and retries only tools/list among post-initialize service operations. ## Validation - cargo fmt --package codex-rmcp-client - cargo test -p codex-rmcp-client --test streamable_http_recovery
This commit is contained in:
1 parent
9316acf9b2
commit
490340ffcf
7 files changed
+986
-40
No files matched your search
@@ -0,0 +1,74 @@
|
||||
use std::any::TypeId;
|
||||
|
||||
use codex_exec_server::ExecServerError;
|
||||
use pretty_assertions::assert_eq;
|
||||
use rmcp::transport::DynamicTransportError;
|
||||
use rmcp::transport::streamable_http_client::StreamableHttpError;
|
||||
|
||||
use crate::http_client_adapter::StreamableHttpClientAdapterError;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn retryable_initialize_error_includes_initialized_notification_context() {
|
||||
let contexts = [
|
||||
"send initialize request",
|
||||
"send initialized notification",
|
||||
"receive initialize response",
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
contexts.map(|context| {
|
||||
RmcpClient::is_retryable_client_initialize_error(&retryable_initialize_error(context))
|
||||
}),
|
||||
[true, true, false],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retryable_streamable_http_error_includes_remote_body_stream_failure() {
|
||||
let errors = [
|
||||
StreamableHttpError::Client(StreamableHttpClientAdapterError::HttpRequest(
|
||||
ExecServerError::HttpRequest("error sending request for url".to_string()),
|
||||
)),
|
||||
StreamableHttpError::Client(StreamableHttpClientAdapterError::HttpRequest(
|
||||
ExecServerError::Server {
|
||||
code: JSON_RPC_INTERNAL_ERROR_CODE,
|
||||
message: "http/request failed: error sending request for url".to_string(),
|
||||
},
|
||||
)),
|
||||
StreamableHttpError::Client(StreamableHttpClientAdapterError::HttpRequest(
|
||||
ExecServerError::Protocol(
|
||||
"http response stream `http-1` failed: exec-server transport disconnected"
|
||||
.to_string(),
|
||||
),
|
||||
)),
|
||||
StreamableHttpError::Client(StreamableHttpClientAdapterError::HttpRequest(
|
||||
ExecServerError::Protocol(
|
||||
"http response stream `http-1` received seq 2, expected 1".to_string(),
|
||||
),
|
||||
)),
|
||||
StreamableHttpError::UnexpectedServerResponse("HTTP 502: upstream failure".into()),
|
||||
StreamableHttpError::UnexpectedServerResponse("HTTP 400: bad request".into()),
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
errors.map(|error| RmcpClient::is_retryable_streamable_http_error(&error)),
|
||||
[true, true, true, false, true, false],
|
||||
);
|
||||
}
|
||||
|
||||
fn retryable_initialize_error(context: &'static str) -> rmcp::service::ClientInitializeError {
|
||||
rmcp::service::ClientInitializeError::TransportError {
|
||||
error: DynamicTransportError::from_parts(
|
||||
"streamable_http",
|
||||
TypeId::of::<()>(),
|
||||
Box::new(StreamableHttpError::Client(
|
||||
StreamableHttpClientAdapterError::HttpRequest(ExecServerError::HttpRequest(
|
||||
"error sending request for url".to_string(),
|
||||
)),
|
||||
)),
|
||||
),
|
||||
context: context.into(),
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user