Remove app-server's direct reqwest dependency (#39293)

## Why

`codex-http-client` is the intended owner of `reqwest`, while direct first-party
dependencies are tracked as migration debt.

## What changed

- Build app-server test HTTP clients through `HttpClientBuilder`.
- Use `http` and `url` types directly where only protocol types are needed.
- Remove `reqwest` from `codex-app-server` and its temporary dependency-policy
  exception.

GitOrigin-RevId: ff9f14a54bc20ec1ea39050bbe08c381ee376119
This commit is contained in:
Celia Chen
2026-08-18 22:00:25 +00:00
committed by copyberry
parent 633bd4abf7
commit 17de14558b
9 changed files with 38 additions and 39 deletions
+1 -1
View File
@@ -2053,10 +2053,10 @@ dependencies = [
"flate2",
"futures",
"hmac 0.12.1",
"http 1.4.0",
"opentelemetry",
"opentelemetry_sdk",
"pretty_assertions",
"reqwest 0.12.28",
"rmcp",
"serde",
"serde_json",
+1 -1
View File
@@ -129,10 +129,10 @@ codex-utils-cargo-bin = { workspace = true }
core_test_support = { workspace = true }
flate2 = { workspace = true }
hmac = { workspace = true }
http = { workspace = true }
opentelemetry = { workspace = true }
opentelemetry_sdk = { workspace = true }
pretty_assertions = { workspace = true }
reqwest = { workspace = true, features = ["rustls-tls"] }
rmcp = { workspace = true, default-features = false, features = [
"elicitation",
"server",
@@ -1406,6 +1406,7 @@ mod tests {
use super::*;
use codex_backend_client::TokenUsageProfileDailyBucket;
use codex_backend_client::TokenUsageProfileStats;
use http::StatusCode;
use pretty_assertions::assert_eq;
#[test]
@@ -1477,9 +1478,9 @@ mod tests {
#[test]
fn workspace_messages_feature_disabled_only_for_not_found() {
let cases = [
(reqwest::StatusCode::NOT_FOUND, true),
(reqwest::StatusCode::UNAUTHORIZED, false),
(reqwest::StatusCode::FORBIDDEN, false),
(StatusCode::NOT_FOUND, true),
(StatusCode::UNAUTHORIZED, false),
(StatusCode::FORBIDDEN, false),
];
for (status, expected) in cases {
@@ -478,6 +478,7 @@ mod tests {
use codex_rollout::RolloutLine;
use core_test_support::responses::start_mock_server;
use core_test_support::test_codex::test_codex;
use http::HeaderMap;
use pretty_assertions::assert_eq;
#[tokio::test]
@@ -790,9 +791,7 @@ mod tests {
#[test]
fn tool_cache_feedback_attachments_include_directory_cache_without_account_id() {
let codex_home = tempfile::tempdir().expect("create tempdir");
let auth = CodexAuth::Headers(codex_login::AuthHeaders::new(
reqwest::header::HeaderMap::new(),
));
let auth = CodexAuth::Headers(codex_login::AuthHeaders::new(HeaderMap::new()));
let directory_cache_context = ConnectorDirectoryCacheContext::new(
codex_home.path().to_path_buf(),
ConnectorDirectoryCacheKey::new(
@@ -40,6 +40,7 @@ use codex_app_server_protocol::ServerRequest;
use codex_app_server_protocol::TurnCompletedNotification;
use codex_app_server_protocol::TurnStatus;
use codex_config::types::AuthCredentialsStoreMode;
use codex_http_client::HttpClientBuilder;
use codex_login::AuthDotJson;
use codex_login::AuthKeyringBackendKind;
use codex_login::CLIENT_ID_OVERRIDE_ENV_VAR;
@@ -2162,9 +2163,9 @@ async fn login_account_chatgpt_redirects_to_hosted_success_page() -> Result<()>
.query_pairs()
.find_map(|(key, value)| (key == "state").then(|| value.into_owned()))
.ok_or_else(|| anyhow::anyhow!("missing state"))?;
let client = reqwest::Client::builder()
.redirect(reqwest::redirect::Policy::none())
.build()?;
let client = HttpClientBuilder::new()
.without_redirects()
.build_direct()?;
let token_redirect_uri = callback_url.clone();
let mut callback_url = Url::parse(&callback_url)?;
@@ -20,12 +20,15 @@ use codex_app_server_protocol::ThreadLoadedListResponse;
use codex_app_server_protocol::ThreadStartParams;
use codex_app_server_protocol::ThreadStartResponse;
use codex_core::config::set_project_trust_level;
use codex_http_client::HttpClient;
use codex_http_client::HttpClientBuilder;
use codex_http_client::HttpResponse;
use codex_protocol::config_types::TrustLevel;
use futures::SinkExt;
use futures::StreamExt;
use hmac::Hmac;
use hmac::Mac;
use reqwest::StatusCode;
use http::StatusCode;
use serde_json::json;
use sha2::Sha256;
use std::net::SocketAddr;
@@ -214,7 +217,7 @@ async fn websocket_transport_serves_health_endpoints_on_same_listener() -> Resul
create_config_toml(codex_home.path(), &server.uri(), "never")?;
let (mut process, bind_addr) = spawn_websocket_server(codex_home.path()).await?;
let client = reqwest::Client::new();
let client = HttpClientBuilder::new().build_direct()?;
let readyz = http_get(&client, bind_addr, "/readyz").await?;
assert_eq!(readyz.status(), StatusCode::OK);
@@ -641,11 +644,7 @@ async fn run_websocket_server_to_completion_with_args(
.context("failed to run websocket app-server")
}
async fn http_get(
client: &reqwest::Client,
bind_addr: SocketAddr,
path: &str,
) -> Result<reqwest::Response> {
async fn http_get(client: &HttpClient, bind_addr: SocketAddr, path: &str) -> Result<HttpResponse> {
let connectable_bind_addr = connectable_bind_addr(bind_addr);
let deadline = Instant::now() + DEFAULT_READ_TIMEOUT;
loop {
@@ -21,6 +21,7 @@ use codex_app_server_protocol::ThreadStartResponse;
use codex_app_server_protocol::TurnStartParams;
use codex_app_server_protocol::TurnStartResponse;
use codex_app_server_protocol::UserInput;
use codex_http_client::HttpClientBuilder;
use codex_utils_path_uri::PathUri;
use core_test_support::responses;
use core_test_support::stdio_server_bin;
@@ -48,6 +49,7 @@ use tempfile::TempDir;
use tokio::net::TcpListener;
use tokio::sync::mpsc;
use tokio::time::timeout;
use url::Url;
const DEFAULT_READ_TIMEOUT: Duration = Duration::from_secs(20);
const EXECUTOR_HTTP_MCP_URL: &str = "http://executor-only.invalid/mcp";
@@ -295,7 +297,7 @@ startup_timeout_sec = 10
.await?;
let response: McpServerOauthLoginResponse =
timeout(DEFAULT_READ_TIMEOUT, app_server.read_response(request_id)).await??;
let authorization_url = reqwest::Url::parse(&response.authorization_url)?;
let authorization_url = Url::parse(&response.authorization_url)?;
let parameters = authorization_url
.query_pairs()
.into_owned()
@@ -308,14 +310,13 @@ startup_timeout_sec = 10
registration_request_rx.try_recv().is_err(),
"configured OAuth client must skip dynamic registration"
);
let mut callback_url = reqwest::Url::parse(&parameters["redirect_uri"])?;
let mut callback_url = Url::parse(&parameters["redirect_uri"])?;
callback_url
.query_pairs_mut()
.append_pair("code", "configured-test-code")
.append_pair("state", &parameters["state"]);
reqwest::Client::builder()
.no_proxy()
.build()?
HttpClientBuilder::new()
.build_direct()?
.get(callback_url)
.send()
.await?
@@ -350,7 +351,7 @@ startup_timeout_sec = 10
.authorization_url
.starts_with("https://oauth-only.invalid/authorize?")
);
let authorization_url = reqwest::Url::parse(&response.authorization_url)?;
let authorization_url = Url::parse(&response.authorization_url)?;
let client_id = authorization_url
.query_pairs()
.find_map(|(key, value)| (key == "client_id").then(|| value.into_owned()));
@@ -371,15 +372,14 @@ startup_timeout_sec = 10
registration_request["redirect_uris"],
json!([redirect_uri.clone()])
);
let mut callback_url = reqwest::Url::parse(&redirect_uri)?;
let mut callback_url = Url::parse(&redirect_uri)?;
assert_eq!(callback_url.port(), Some(plugin_callback_port));
callback_url
.query_pairs_mut()
.append_pair("code", "executor-test-code")
.append_pair("state", &state);
reqwest::Client::builder()
.no_proxy()
.build()?
HttpClientBuilder::new()
.build_direct()?
.get(callback_url)
.send()
.await?
@@ -28,6 +28,7 @@ use codex_app_server_protocol::RequestId;
use codex_app_server_protocol::ThreadStartParams;
use codex_app_server_protocol::ThreadStartResponse;
use codex_core::config::set_project_trust_level;
use codex_http_client::HttpClientBuilder;
use codex_protocol::config_types::TrustLevel;
use core_test_support::stdio_server_bin;
use pretty_assertions::assert_eq;
@@ -53,6 +54,7 @@ use tokio::sync::mpsc;
use tokio::task::JoinHandle;
use tokio::time::sleep;
use tokio::time::timeout;
use url::Url;
use wiremock::Mock;
use wiremock::MockServer;
use wiremock::ResponseTemplate;
@@ -118,16 +120,15 @@ async fn oauth_login_uses_http_headers_helper() -> Result<()> {
.await?;
let response: McpServerOauthLoginResponse =
timeout(DEFAULT_READ_TIMEOUT, mcp.read_response(request_id)).await??;
let authorization_url = reqwest::Url::parse(&response.authorization_url)?;
let authorization_url = Url::parse(&response.authorization_url)?;
let query: BTreeMap<_, _> = authorization_url.query_pairs().into_owned().collect();
let mut callback_url = reqwest::Url::parse(&query["redirect_uri"])?;
let mut callback_url = Url::parse(&query["redirect_uri"])?;
callback_url
.query_pairs_mut()
.append_pair("code", "test-code")
.append_pair("state", &query["state"]);
reqwest::Client::builder()
.no_proxy()
.build()?
HttpClientBuilder::new()
.build_direct()?
.get(callback_url)
.send()
.await?
@@ -348,13 +349,13 @@ async fn oauth_login_automatically_selects_callback_specific_cimd_without_metada
.await?;
let response: McpServerOauthLoginResponse =
timeout(DEFAULT_READ_TIMEOUT, app_server.read_response(request_id)).await??;
let authorization_url = reqwest::Url::parse(&response.authorization_url)?;
let authorization_url = Url::parse(&response.authorization_url)?;
let parameters = authorization_url
.query_pairs()
.into_owned()
.collect::<BTreeMap<String, String>>();
let redirect_uri = parameters["redirect_uri"].clone();
let mut callback_url = reqwest::Url::parse(&redirect_uri)?;
let mut callback_url = Url::parse(&redirect_uri)?;
let callback_id = callback_url
.path()
.strip_prefix("/callback/")
@@ -371,9 +372,8 @@ async fn oauth_login_automatically_selects_callback_specific_cimd_without_metada
.query_pairs_mut()
.append_pair("code", "cimd-authorization-code")
.append_pair("state", &parameters["state"]);
reqwest::Client::builder()
.no_proxy()
.build()?
HttpClientBuilder::new()
.build_direct()?
.get(callback_url)
.send()
.await?
-1
View File
@@ -244,7 +244,6 @@ deny = [
# upstream HTTP transport independently of Codex.
"codex-responses-api-proxy",
# Temporary migration exceptions.
"codex-app-server",
"codex-otel",
# Third-party crates that own their reqwest integration. These are not part of the
# first-party migration count above.