wip: #6784 force_http1 stream-open fix (unverified, local only)

This commit is contained in:
Hunter B committed 2026-09-29 17:55:04 -07:00
1 parent 61ecea887f
commit d0cc92f2a6
6 files changed
+73 -20

No files matched your search

+52 -5
View File
@@ -353,6 +353,9 @@ pub struct CodewhaleClient {
/// Bounded wait for SSE response headers, resolved once from
/// `[tui].stream_open_timeout_secs` / `CODEWHALE_STREAM_OPEN_TIMEOUT_SECS`.
pub(super) stream_open_timeout: Duration,
/// HTTP/1.1 pin resolved once from `Config::force_http1` (#6700); the
/// single source every client builder and stream open reads.
pub(super) force_http1: bool,
}
const CONNECTION_FAILURE_THRESHOLD: u32 = 2;
@@ -642,6 +645,7 @@ impl Clone for CodewhaleClient {
reasoning_stream_style: self.reasoning_stream_style.clone(),
stream_idle_timeout: self.stream_idle_timeout,
stream_open_timeout: self.stream_open_timeout,
force_http1: self.force_http1,
}
}
}
@@ -1389,9 +1393,9 @@ fn build_speech_synthesis_body(
// === CodewhaleClient ===
/// Returns true when CODEWHALE_FORCE_HTTP1 (legacy alias: DEEPSEEK_FORCE_HTTP1)
/// is set to a truthy value (`1`, `true`, `yes`, `on`, case-insensitive). Used
/// by `build_http_client` to opt out of HTTP/2 entirely when a provider's edge
/// mishandles long-lived H2 streams (#103). Anything else (unset, `0`,
/// is set to a truthy value (`1`, `true`, `yes`, `on`, case-insensitive). Read
/// only by `Config::force_http1`, which ORs it with `[tui].force_http1`; every
/// client builder and stream open takes that resolved value (#103, #6700). Anything else (unset, `0`,
/// `false`, ...) leaves HTTP/2 on.
pub(crate) fn force_http1_from_env() -> bool {
std::env::var("CODEWHALE_FORCE_HTTP1")
@@ -1668,7 +1672,7 @@ impl CodewhaleClient {
.redirect(reqwest::redirect::Policy::none())
.build()?;
// Always keep an HTTP/1.1 twin for automatic stream-header fallback
// when H2 stalls. When CODEWHALE_FORCE_HTTP1 is set, both clients are
// when H2 stalls. When `force_http1` is pinned, both clients are
// HTTP/1.1 and the fallback is a no-op retry path.
let http1_client = Self::http_client_builder_with_auth_mode(
&api_key,
@@ -1722,6 +1726,7 @@ impl CodewhaleClient {
reasoning_stream_style,
stream_idle_timeout,
stream_open_timeout,
force_http1,
})
}
@@ -2048,7 +2053,7 @@ impl CodewhaleClient {
.http2_keep_alive_interval(Some(Duration::from_secs(15)))
.http2_keep_alive_timeout(Duration::from_secs(20))
.min_tls_version(reqwest::tls::Version::TLS_1_2);
if force_http1 || force_http1_from_env() {
if force_http1 {
builder = builder.http1_only();
}
if let Ok(cert_path) = std::env::var("SSL_CERT_FILE")
@@ -13774,6 +13779,48 @@ mod tests {
}
}
/// #6700: `[tui].force_http1` alone (no env var) must reach the stream-open
/// policy every adapter uses, not just the client builders.
#[test]
fn tui_force_http1_key_pins_stream_open_policy() {
let _lock = FORCE_HTTP1_ENV_LOCK.lock().unwrap();
let _env = crate::test_support::lock_test_env();
let _codewhale = crate::test_support::EnvVarGuard::remove("CODEWHALE_FORCE_HTTP1");
let _deepseek = crate::test_support::EnvVarGuard::remove("DEEPSEEK_FORCE_HTTP1");
let client_for = |force_http1: Option<bool>| {
CodewhaleClient::new(&Config {
provider: Some("zai".to_string()),
providers: Some(ProvidersConfig {
zai: ProviderConfig {
api_key: Some("zai-force-http1-key".to_string()),
..ProviderConfig::default()
},
..ProvidersConfig::default()
}),
tui: Some(crate::config::TuiConfig {
force_http1,
..crate::config::TuiConfig::default()
}),
..Config::default()
})
.expect("client builds")
};
let pinned = client_for(Some(true));
assert!(pinned.force_http1);
assert_eq!(
pinned.stream_open_request().policy,
stream_entry::StreamHttpPolicy::Http1Only
);
let unpinned = client_for(None);
assert!(!unpinned.force_http1);
assert_eq!(
unpinned.stream_open_request().policy,
stream_entry::StreamHttpPolicy::DualWithH1Fallback
);
}
#[test]
fn redact_url_for_display_masks_userinfo_and_sensitive_query_values() {
let redacted = redact_url_for_display(
+1 -4
View File
@@ -256,10 +256,7 @@ impl CodewhaleClient {
body: &Value,
) -> Result<reqwest::Response> {
let url = self.messages_transport_url(url);
let open_req = super::stream_entry::StreamOpenRequest::new(
self.stream_open_timeout,
self.stream_idle_timeout,
);
let open_req = self.stream_open_request();
let opened = super::stream_entry::open_sse_response(&open_req, |policy| {
let url = url.clone();
async move {
+1 -4
View File
@@ -1330,10 +1330,7 @@ impl CodewhaleClient {
url: &str,
body: &Value,
) -> Result<(reqwest::Response, Duration)> {
let open_req = super::stream_entry::StreamOpenRequest::new(
self.stream_open_timeout,
self.stream_idle_timeout,
);
let open_req = self.stream_open_request();
let idle_timeout = open_req.idle_timeout;
let response = super::stream_entry::open_sse_response(&open_req, |policy| async move {
match policy {
+1 -4
View File
@@ -180,10 +180,7 @@ impl CodewhaleClient {
let account_id = self.codex_account_id.clone();
let request_body =
serde_json::to_vec(&body).context("Failed to serialize Responses API request body")?;
let open_req = super::stream_entry::StreamOpenRequest::new(
self.stream_open_timeout,
self.stream_idle_timeout,
);
let open_req = self.stream_open_request();
let response = super::stream_entry::open_sse_response(&open_req, |policy| {
let url = url.clone();
let account_id = account_id.clone();
+17 -2
View File
@@ -159,10 +159,12 @@ pub struct StreamOpenRequest {
}
impl StreamOpenRequest {
/// `force_http1` is the client's resolved pin (`Config::force_http1`:
/// `[tui].force_http1` or `CODEWHALE_FORCE_HTTP1`), never re-read here.
#[must_use]
pub fn new(open_timeout: Duration, idle_timeout: Duration) -> Self {
pub fn new(force_http1: bool, open_timeout: Duration, idle_timeout: Duration) -> Self {
Self {
policy: if super::force_http1_from_env() {
policy: if force_http1 {
StreamHttpPolicy::Http1Only
} else {
StreamHttpPolicy::DualWithH1Fallback
@@ -180,6 +182,19 @@ impl StreamOpenRequest {
}
}
impl super::CodewhaleClient {
/// The open request every streaming adapter starts from, carrying this
/// client's resolved HTTP/1.1 pin and timeouts (#6700).
#[must_use]
pub(super) fn stream_open_request(&self) -> StreamOpenRequest {
StreamOpenRequest::new(
self.force_http1,
self.stream_open_timeout,
self.stream_idle_timeout,
)
}
}
/// Select the HTTP client for a stream open attempt.
#[must_use]
pub fn client_for_policy<'a>(
+1 -1
View File
@@ -207,7 +207,7 @@ impl CodewhaleClient {
&base_url,
WireFormat::ChatCompletions,
false,
false,
client.force_http1,
config.connect_timeout(),
)?
.build()?;