mirror of
https://github.com/openai/codex.git
synced 2026-10-19 17:28:46 +00:00
login: route raw auth flows through HTTP client (#31637)
## Why Login already honors `respect_system_proxy`, but several login-owned auth flows still construct and pass around raw `reqwest::Client` values. That keeps those request paths coupled to the underlying transport and leaves `codex-login` on the temporary direct-`reqwest` allowlist introduced by #31431. Auth endpoints also have a stricter logging boundary than ordinary API requests: custom issuer URLs and response headers may contain credentials. Moving these requests behind the shared HTTP abstraction must preserve that boundary while retaining route-aware proxy and custom-CA behavior. This is a bounded login migration. The separate Agent Identity and shared default-client compatibility migrations remain follow-up work. ## What changed - Add `HttpClientFactory::build_client` to construct the shared `HttpClient` abstraction for a resolved destination and route class. - Add a route-aware construction path that suppresses request URL, response-header, and transport-error diagnostics for sensitive auth endpoints. - Route device-code user-code/polling requests, OAuth authorization-code exchange, and API-key token exchange through `HttpClient`. - Build the revoke timeout test client through the same factory API. - Use the transport-neutral `http::StatusCode` in the migrated device-code flow. - Add an end-to-end log-capture regression test covering successful responses and transport failures after `RequestBuilder` transformations. ## Review guidance The request behavior is intended to be unchanged: each issuer/token endpoint selects the same auth route, including the existing system/PAC proxy and custom-CA handling, and raw auth clients still omit Codex default headers. The intentional logging change is limited to raw auth requests, whose URL userinfo, query credentials, response headers, and transport errors must not cross the auth redaction boundary. This PR deliberately does **not** remove `codex-login` from #31431's allowlist. The remaining direct `reqwest` surface belongs primarily to: - Agent Identity APIs that still accept `reqwest::Client`. - Exported default-client compatibility helpers used by other workspace crates. - A small number of tests and concrete error/header types. ## Testing - `cargo check -p codex-http-client -p codex-login --tests` - `just test -p codex-http-client` (41 tests) - `just test -p codex-login` (155 tests) - `just bazel-lock-check` --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/openai/codex/pull/31637). * #31837 * #31828 * #31825 * #31821 * __->__ #31637
This commit is contained in:
@@ -16,11 +16,26 @@ use tracing_opentelemetry::OpenTelemetrySpanExt;
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct HttpClient {
|
||||
inner: reqwest::Client,
|
||||
request_logging: RequestLogging,
|
||||
}
|
||||
|
||||
impl HttpClient {
|
||||
pub fn new(inner: reqwest::Client) -> Self {
|
||||
Self { inner }
|
||||
Self {
|
||||
inner,
|
||||
request_logging: RequestLogging::Enabled,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a client that suppresses request URL and response-header diagnostics.
|
||||
///
|
||||
/// Use this for authentication endpoints whose URLs or headers may contain credentials that
|
||||
/// are redacted by the caller above the HTTP transport boundary.
|
||||
pub(crate) fn new_without_request_logging(inner: reqwest::Client) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
request_logging: RequestLogging::Disabled,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get<U>(&self, url: U) -> RequestBuilder
|
||||
@@ -42,24 +57,42 @@ impl HttpClient {
|
||||
U: IntoUrl,
|
||||
{
|
||||
let url_str = url.as_str().to_string();
|
||||
RequestBuilder::new(self.inner.request(method.clone(), url), method, url_str)
|
||||
RequestBuilder::new(
|
||||
self.inner.request(method.clone(), url),
|
||||
method,
|
||||
url_str,
|
||||
self.request_logging,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum RequestLogging {
|
||||
Enabled,
|
||||
Disabled,
|
||||
}
|
||||
|
||||
#[must_use = "requests are not sent unless `send` is awaited"]
|
||||
#[derive(Debug)]
|
||||
pub struct RequestBuilder {
|
||||
builder: reqwest::RequestBuilder,
|
||||
method: Method,
|
||||
url: String,
|
||||
request_logging: RequestLogging,
|
||||
}
|
||||
|
||||
impl RequestBuilder {
|
||||
fn new(builder: reqwest::RequestBuilder, method: Method, url: String) -> Self {
|
||||
fn new(
|
||||
builder: reqwest::RequestBuilder,
|
||||
method: Method,
|
||||
url: String,
|
||||
request_logging: RequestLogging,
|
||||
) -> Self {
|
||||
Self {
|
||||
builder,
|
||||
method,
|
||||
url,
|
||||
request_logging,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,6 +101,7 @@ impl RequestBuilder {
|
||||
builder: f(self.builder),
|
||||
method: self.method,
|
||||
url: self.url,
|
||||
request_logging: self.request_logging,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,26 +149,30 @@ impl RequestBuilder {
|
||||
|
||||
match self.builder.headers(headers).send().await {
|
||||
Ok(response) => {
|
||||
tracing::debug!(
|
||||
method = %self.method,
|
||||
url = %self.url,
|
||||
status = %response.status(),
|
||||
headers = ?response.headers(),
|
||||
version = ?response.version(),
|
||||
"Request completed"
|
||||
);
|
||||
if self.request_logging == RequestLogging::Enabled {
|
||||
tracing::debug!(
|
||||
method = %self.method,
|
||||
url = %self.url,
|
||||
status = %response.status(),
|
||||
headers = ?response.headers(),
|
||||
version = ?response.version(),
|
||||
"Request completed"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
Err(error) => {
|
||||
let status = error.status();
|
||||
tracing::debug!(
|
||||
method = %self.method,
|
||||
url = %self.url,
|
||||
status = status.map(|s| s.as_u16()),
|
||||
error = %error,
|
||||
"Request failed"
|
||||
);
|
||||
if self.request_logging == RequestLogging::Enabled {
|
||||
let status = error.status();
|
||||
tracing::debug!(
|
||||
method = %self.method,
|
||||
url = %self.url,
|
||||
status = status.map(|s| s.as_u16()),
|
||||
error = %error,
|
||||
"Request failed"
|
||||
);
|
||||
}
|
||||
Err(error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ use std::time::Instant;
|
||||
|
||||
use crate::custom_ca::BuildCustomCaTransportError;
|
||||
use crate::custom_ca::build_reqwest_client_with_custom_ca;
|
||||
use crate::default_client::HttpClient;
|
||||
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||
use sha2::Digest;
|
||||
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||
@@ -158,6 +159,26 @@ impl HttpClientFactory {
|
||||
)
|
||||
}
|
||||
|
||||
/// Builds an HTTP client for a concrete outbound route.
|
||||
pub fn build_client(
|
||||
&self,
|
||||
request_url: &str,
|
||||
route_class: ClientRouteClass,
|
||||
) -> Result<HttpClient, BuildRouteAwareHttpClientError> {
|
||||
self.build_reqwest_client(reqwest::Client::builder(), request_url, route_class)
|
||||
.map(HttpClient::new)
|
||||
}
|
||||
|
||||
/// Builds a route-aware client without request URL or response-header diagnostics.
|
||||
pub fn build_client_without_request_logging(
|
||||
&self,
|
||||
request_url: &str,
|
||||
route_class: ClientRouteClass,
|
||||
) -> Result<HttpClient, BuildRouteAwareHttpClientError> {
|
||||
self.build_reqwest_client(reqwest::Client::builder(), request_url, route_class)
|
||||
.map(HttpClient::new_without_request_logging)
|
||||
}
|
||||
|
||||
/// Builds a reqwest client for a concrete outbound route.
|
||||
pub fn build_reqwest_client(
|
||||
&self,
|
||||
|
||||
Reference in New Issue
Block a user