mirror of
https://github.com/openai/codex.git
synced 2026-09-05 15:18:41 +00:00
## Why Backend-backed features need to use the same outbound routing configuration as the rest of Codex. ## What changed - Build the backend client on a route-aware client pool supplied by `HttpClientFactory`. - Pass the configured factory through app-server account requests, cloud config loading, cloud tasks, and memory rate-limit checks. - Preserve task query encoding, authentication headers, JSON request bodies, and ChatGPT cookie handling during the migration. ## Testing - Add backend-client tests for proxy-policy propagation, task query encoding, bearer authentication, and JSON request bodies. GitOrigin-RevId: c17d162e0dce9cec758df7cdeecf982207c6512b
145 lines
5.0 KiB
Rust
145 lines
5.0 KiB
Rust
use std::io::Read;
|
|
use std::io::Write;
|
|
use std::sync::Arc;
|
|
use std::time::Duration;
|
|
|
|
use codex_http_client::HttpClientFactory;
|
|
use codex_http_client::OutboundProxyPolicy;
|
|
use pretty_assertions::assert_eq;
|
|
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn client_preserves_supplied_http_client_factory_policy() {
|
|
let client = Client::new(
|
|
"https://example.test",
|
|
HttpClientFactory::new(OutboundProxyPolicy::RespectSystemProxy),
|
|
);
|
|
|
|
assert_eq!(
|
|
client.http.outbound_proxy_policy(),
|
|
OutboundProxyPolicy::RespectSystemProxy
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn list_tasks_url_omits_empty_query_and_encodes_all_parameters() {
|
|
let client = Client::new(
|
|
"https://example.test",
|
|
HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault),
|
|
);
|
|
|
|
assert_eq!(
|
|
client
|
|
.list_tasks_url(
|
|
/*limit*/ None, /*task_filter*/ None, /*environment_id*/ None,
|
|
/*cursor*/ None,
|
|
)
|
|
.unwrap(),
|
|
"https://example.test/api/codex/tasks/list"
|
|
);
|
|
assert_eq!(
|
|
client
|
|
.list_tasks_url(
|
|
/*limit*/ Some(10),
|
|
/*task_filter*/ Some("mine / shared"),
|
|
/*environment_id*/ Some("env&one"),
|
|
/*cursor*/ Some("next=page"),
|
|
)
|
|
.unwrap(),
|
|
"https://example.test/api/codex/tasks/list?limit=10&task_filter=mine+%2F+shared&cursor=next%3Dpage&environment_id=env%26one"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn migrated_requests_preserve_query_auth_and_json_body() {
|
|
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("HTTP listener should bind");
|
|
let address = listener
|
|
.local_addr()
|
|
.expect("HTTP listener should have an address");
|
|
let server = std::thread::spawn(move || {
|
|
let mut requests = Vec::new();
|
|
for body in [r#"{"items":[]}"#, r#"{"task":{"id":"task-created"}}"#] {
|
|
let (mut stream, _) = listener.accept().expect("HTTP listener should accept");
|
|
stream
|
|
.set_read_timeout(Some(Duration::from_secs(2)))
|
|
.expect("HTTP stream should get a read timeout");
|
|
let mut request = Vec::new();
|
|
let mut buffer = [0_u8; 4096];
|
|
loop {
|
|
let size = stream.read(&mut buffer).expect("HTTP request should read");
|
|
if size == 0 {
|
|
break;
|
|
}
|
|
request.extend_from_slice(&buffer[..size]);
|
|
let Some(headers_end) = request.windows(4).position(|part| part == b"\r\n\r\n")
|
|
else {
|
|
continue;
|
|
};
|
|
let headers = String::from_utf8_lossy(&request[..headers_end]);
|
|
let content_length = headers
|
|
.lines()
|
|
.find_map(|line| {
|
|
let (name, value) = line.split_once(':')?;
|
|
name.eq_ignore_ascii_case("content-length")
|
|
.then(|| value.trim().parse::<usize>().ok())
|
|
.flatten()
|
|
})
|
|
.unwrap_or(0);
|
|
if request.len() >= headers_end + 4 + content_length {
|
|
break;
|
|
}
|
|
}
|
|
requests.push(String::from_utf8(request).expect("request should be UTF-8"));
|
|
write!(
|
|
stream,
|
|
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
|
|
body.len()
|
|
)
|
|
.expect("HTTP response should write");
|
|
}
|
|
requests
|
|
});
|
|
let client = Client::new(
|
|
format!("http://{address}"),
|
|
HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault),
|
|
)
|
|
.with_auth_provider(Arc::new(codex_model_provider::BearerAuthProvider::new(
|
|
"request-token".to_string(),
|
|
)));
|
|
|
|
let tasks = client
|
|
.list_tasks(
|
|
Some(10),
|
|
Some("mine / shared"),
|
|
Some("env&one"),
|
|
Some("next=page"),
|
|
)
|
|
.await
|
|
.expect("list request should succeed");
|
|
let task_id = client
|
|
.create_task(serde_json::json!({ "prompt": "hello" }))
|
|
.await
|
|
.expect("create request should succeed");
|
|
let requests = server.join().expect("HTTP server should finish");
|
|
|
|
assert_eq!(tasks, PaginatedListTaskListItem::new(Vec::new()));
|
|
assert_eq!(task_id, "task-created");
|
|
assert_eq!(requests.len(), 2);
|
|
assert!(requests[0].starts_with(
|
|
"GET /api/codex/tasks/list?limit=10&task_filter=mine+%2F+shared&cursor=next%3Dpage&environment_id=env%26one HTTP/1.1\r\n"
|
|
));
|
|
assert!(
|
|
requests[0]
|
|
.to_ascii_lowercase()
|
|
.contains("authorization: bearer request-token\r\n")
|
|
);
|
|
assert!(requests[1].starts_with("POST /api/codex/tasks HTTP/1.1\r\n"));
|
|
assert!(
|
|
requests[1]
|
|
.to_ascii_lowercase()
|
|
.contains("authorization: bearer request-token\r\n")
|
|
);
|
|
assert!(requests[1].ends_with(r#"{"prompt":"hello"}"#));
|
|
}
|