mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Escape turn metadata headers as ASCII JSON (#19620)
## Why `x-codex-turn-metadata` is sent as an HTTP/WebSocket header, but Codex was serializing the metadata JSON with raw UTF-8 string contents. When a workspace path contains non-ASCII characters, common HTTP stacks can reject or corrupt that header before the request reaches the provider. Fixes #17468. Also addresses the duplicate WebSocket report in #19581. ## What changed - Added `codex_utils_string::to_ascii_json_string`, a shared helper that serializes JSON normally while escaping non-ASCII string content as `\uXXXX`. - Switched turn metadata header serialization, including merged Responses API client metadata, to use the ASCII-safe JSON helper. - Added coverage for non-ASCII workspace paths and non-ASCII client metadata while preserving the same parsed JSON values. ## Verification - `cargo test -p codex-utils-string` - `cargo test -p codex-core turn_metadata` - `just bazel-lock-check`
This commit is contained in:
@@ -4,6 +4,7 @@ use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
use std::sync::RwLock;
|
||||
|
||||
use codex_utils_string::to_ascii_json_string;
|
||||
use serde::Serialize;
|
||||
use serde_json::Value;
|
||||
use tokio::task::JoinHandle;
|
||||
@@ -71,7 +72,7 @@ pub(crate) struct TurnMetadataBag {
|
||||
|
||||
impl TurnMetadataBag {
|
||||
fn to_header_value(&self) -> Option<String> {
|
||||
serde_json::to_string(self).ok()
|
||||
to_ascii_json_string(self).ok()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,7 +102,7 @@ fn merge_turn_metadata(
|
||||
.or_insert_with(|| Value::String(value.clone()));
|
||||
}
|
||||
}
|
||||
serde_json::to_string(&metadata).ok()
|
||||
to_ascii_json_string(&metadata).ok()
|
||||
}
|
||||
|
||||
fn build_turn_metadata_bag(
|
||||
|
||||
@@ -7,6 +7,7 @@ use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::SubAgentSource;
|
||||
use core_test_support::PathBufExt;
|
||||
use core_test_support::PathExt;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
use tempfile::TempDir;
|
||||
@@ -15,7 +16,7 @@ use tokio::process::Command;
|
||||
#[tokio::test]
|
||||
async fn build_turn_metadata_header_includes_has_changes_for_clean_repo() {
|
||||
let temp_dir = TempDir::new().expect("temp dir");
|
||||
let repo_path = temp_dir.path().join("repo").abs();
|
||||
let repo_path = temp_dir.path().join("repo-東京").abs();
|
||||
std::fs::create_dir_all(&repo_path).expect("create repo");
|
||||
|
||||
Command::new("git")
|
||||
@@ -54,7 +55,16 @@ async fn build_turn_metadata_header_includes_has_changes_for_clean_repo() {
|
||||
let header = build_turn_metadata_header(&repo_path, Some("none"))
|
||||
.await
|
||||
.expect("header");
|
||||
assert!(header.is_ascii());
|
||||
assert!(!header.contains("東京"));
|
||||
let parsed: Value = serde_json::from_str(&header).expect("valid json");
|
||||
let expected_repo_path = repo_path.to_string_lossy().into_owned();
|
||||
let actual_repo_path = parsed
|
||||
.get("workspaces")
|
||||
.and_then(Value::as_object)
|
||||
.and_then(|workspaces| workspaces.keys().next())
|
||||
.expect("workspace path");
|
||||
assert_eq!(actual_repo_path, &expected_repo_path);
|
||||
let workspace = parsed
|
||||
.get("workspaces")
|
||||
.and_then(Value::as_object)
|
||||
@@ -191,6 +201,7 @@ fn turn_metadata_state_merges_client_metadata_without_replacing_reserved_fields(
|
||||
);
|
||||
state.set_responsesapi_client_metadata(HashMap::from([
|
||||
("fiber_run_id".to_string(), "fiber-123".to_string()),
|
||||
("origin".to_string(), "東京".to_string()),
|
||||
("session_id".to_string(), "client-supplied".to_string()),
|
||||
("thread_source".to_string(), "client-supplied".to_string()),
|
||||
(
|
||||
@@ -201,9 +212,12 @@ fn turn_metadata_state_merges_client_metadata_without_replacing_reserved_fields(
|
||||
state.set_turn_started_at_unix_ms(/*turn_started_at_unix_ms*/ 1_700_000_000_123);
|
||||
|
||||
let header = state.current_header_value().expect("header");
|
||||
assert!(header.is_ascii());
|
||||
assert!(!header.contains("東京"));
|
||||
let json: Value = serde_json::from_str(&header).expect("json");
|
||||
|
||||
assert_eq!(json["fiber_run_id"].as_str(), Some("fiber-123"));
|
||||
assert_eq!(json["origin"].as_str(), Some("東京"));
|
||||
assert_eq!(json["session_id"].as_str(), Some("session-a"));
|
||||
assert_eq!(json["thread_source"].as_str(), Some("user"));
|
||||
assert_eq!(json["turn_id"].as_str(), Some("turn-a"));
|
||||
|
||||
Reference in New Issue
Block a user