mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
490340ffcf
## 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
75 lines
2.6 KiB
Rust
75 lines
2.6 KiB
Rust
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(),
|
|
}
|
|
}
|