[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`
This commit is contained in:
xl-openai
2026-06-18 14:29:01 -07:00
committed by GitHub
Unverified
parent 4af7762f01
commit 406062c3af
2 changed files with 49 additions and 14 deletions
@@ -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",
+31 -7
View File
@@ -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 });
}