From 406062c3af8b27c8e1b4b83c485ebe1ae0df874c Mon Sep 17 00:00:00 2001 From: xl-openai Date: Thu, 18 Jun 2026 14:29:01 -0700 Subject: [PATCH] [codex] Preserve remote plugin download status errors (#28863) ## Summary - preserve the original HTTP status when a remote plugin bundle download returns a non-success response - retain at most 8 KiB of the error response body and annotate truncation or body-read failures - add regression coverage for an oversized error response ## Root cause The non-success response path reused the normal size-limited body reader. When an error response exceeded 8 KiB, that reader returned `DownloadTooLarge` before the code constructed `DownloadStatus`, masking the upstream HTTP status and response context. ## Impact Remote plugin installation failures now retain the actionable upstream HTTP status without allowing unbounded error bodies into logs. ## Validation - `just test -p codex-app-server plugin_install_preserves_status_when_remote_bundle_error_body_is_too_large` - `just fmt` - `git diff --check` --- .../tests/suite/v2/plugin_install.rs | 25 ++++++++---- codex-rs/core-plugins/src/remote_bundle.rs | 38 +++++++++++++++---- 2 files changed, 49 insertions(+), 14 deletions(-) diff --git a/codex-rs/app-server/tests/suite/v2/plugin_install.rs b/codex-rs/app-server/tests/suite/v2/plugin_install.rs index 4026bef35..3fc40d2dc 100644 --- a/codex-rs/app-server/tests/suite/v2/plugin_install.rs +++ b/codex-rs/app-server/tests/suite/v2/plugin_install.rs @@ -1010,15 +1010,12 @@ async fn plugin_install_tracks_remote_plugin_analytics_event() -> Result<()> { } #[tokio::test] -async fn plugin_install_errors_when_remote_bundle_download_fails() -> Result<()> { +async fn plugin_install_preserves_status_when_remote_bundle_error_body_is_too_large() -> Result<()> +{ let codex_home = TempDir::new()?; let server = MockServer::start().await; - let bundle_url = mount_remote_plugin_bundle( - &server, - /*status_code*/ 503, - b"bundle temporarily unavailable".to_vec(), - ) - .await; + let bundle_url = + mount_remote_plugin_bundle(&server, /*status_code*/ 503, vec![b'x'; 8 * 1024 + 1]).await; configure_remote_plugin_test(codex_home.path(), &server)?; mount_remote_plugin_detail(&server, REMOTE_PLUGIN_ID, "1.2.3", Some(&bundle_url)).await; mount_empty_remote_installed_plugins(&server).await; @@ -1041,6 +1038,20 @@ async fn plugin_install_errors_when_remote_bundle_download_fails() -> Result<()> assert_eq!(err.error.code, -32603); assert!(err.error.message.contains("failed with status 503")); + assert!( + err.error + .message + .contains("[response body truncated after 8192 bytes]") + ); + assert_eq!( + err.error + .message + .bytes() + .filter(|byte| *byte == b'x') + .count(), + 8192 + ); + assert!(!err.error.message.contains("exceeded maximum size")); wait_for_remote_plugin_request_count( &server, "GET", diff --git a/codex-rs/core-plugins/src/remote_bundle.rs b/codex-rs/core-plugins/src/remote_bundle.rs index cb60d60b7..49e6bd413 100644 --- a/codex-rs/core-plugins/src/remote_bundle.rs +++ b/codex-rs/core-plugins/src/remote_bundle.rs @@ -293,13 +293,37 @@ async fn download_remote_plugin_bundle_with_limit( let url = final_url.to_string(); let status = response.status(); if !status.is_success() { - let body = read_response_body_with_limit( - response, - &url, - /*max_bytes*/ REMOTE_PLUGIN_BUNDLE_ERROR_BODY_MAX_BYTES, - ) - .await?; - let body = String::from_utf8_lossy(&body).to_string(); + let mut response = response; + let mut body = Vec::new(); + let mut body_truncated = false; + let mut body_read_error = None; + loop { + let chunk = match response.chunk().await { + Ok(Some(chunk)) => chunk, + Ok(None) => break, + Err(source) => { + body_read_error = Some(source); + break; + } + }; + let remaining = REMOTE_PLUGIN_BUNDLE_ERROR_BODY_MAX_BYTES as usize - body.len(); + if chunk.len() > remaining { + body.extend_from_slice(&chunk[..remaining]); + body_truncated = true; + break; + } + body.extend_from_slice(&chunk); + } + + let mut body = String::from_utf8_lossy(&body).into_owned(); + if body_truncated { + body.push_str(&format!( + "\n[response body truncated after {REMOTE_PLUGIN_BUNDLE_ERROR_BODY_MAX_BYTES} bytes]" + )); + } + if let Some(source) = body_read_error { + body.push_str(&format!("\n[failed to read response body: {source}]")); + } return Err(RemotePluginBundleInstallError::DownloadStatus { url, status, body }); }