Skip to content

fix(http): pre-warm CA bundle to stop concurrent-subagent TLS races (#131) - #150

Merged
justrach merged 1 commit into
mainfrom
fix/tls-ca-bundle-race-131
Jul 10, 2026
Merged

justrach merged 1 commit into
mainfrom
fix/tls-ca-bundle-race-131

Conversation

@justrach

Copy link
Copy Markdown
Owner

Closes #131.

Root cause — a CA-bundle data race in std.http.Client, triggered by concurrent subagents

Her evidence: 4 gpt-5.5 subagent calls failed with is_error:true, ms:0, resp_bytes:0; #132 names the actual error — network error: TlsInitializationFailed (gave up after 6 attempts). ms:0 = failure at the transport layer before any network round-trip, and it only happens on concurrent subagent calls, never the sequential main agent.

Parallel subagents (workflow/judge) share one std.http.Client across pool threads (subagent.zig .client = ctx.client; http.zig even notes it assumes fetch is thread-safe). On the first HTTPS connect, std.http.Client lazily rescans the CA bundle:

// std/http/Client.zig
if (client.now != null) break :tls;      // skip once loaded
… bundle.rescan(...);
client.now = now;
std.mem.swap(&client.ca_bundle, &bundle); // defer bundle.deinit() frees the old one

and the TLS handshake reads &client.ca_bundle:

// TODO data race here on ca_bundle          ← Zig std's own comment
.client = std.crypto.tls.Client.init(…, .ca = .{ .bundle = .{ .bundle = &client.ca_bundle, … } }, .realtime_now = client.now.?, …)

When several pool threads make their first HTTPS connect simultaneously on a cold client, two both see client.now == null and both rescan → swap → deinit. One thread's swap/deinit yanks the bundle another thread is mid-handshake with → instant error.TlsInitializationFailed. Once client.now is set the path is skipped, so it only bites the first concurrent burst — exactly the intermittent subagent failures she saw.

Fix

prewarmCaBundle() loads the bundle single-threaded right after each client is created (session + oneshot paths), setting client.now. Every later connect — including all concurrent subagents — then skips the rescan and just reads the stable, immutable bundle. No race window, regardless of which client the subagents share.

Verified

zig build · zig build test 152/152 · a normal HTTPS turn still works (the pre-warm is transparent).

Related

#132/#134 (the stall/drop-mislabeled-as-Esc issues) are a separate path already addressed by the #133/#134 stream work; this PR is only the TlsInitializationFailed root cause.

🤖 Generated with Claude Code

…es (#131)

Parallel subagents (workflow/judge) share one std.http.Client across pool
threads. On their first HTTPS connect they each lazily rescan the client's CA
bundle (std.http.Client.zig: rescan → std.mem.swap into client.ca_bundle, then
set client.now; later connects skip it once client.now is set). When several
pool threads hit that cold path at once, two of them both pass the
`client.now == null` check and both rescan+swap+deinit the bundle — one
thread's swap/deinit yanks the bundle another thread is mid-handshake with, so
its tls.Client.init fails instantly with error.TlsInitializationFailed
(ms=0, resp_bytes=0). Zig std flags the race itself: "TODO data race here on
ca_bundle". This is exactly #131 (4 gpt-5.5 subagent calls, ms=0/resp_bytes=0)
and the "TlsInitializationFailed (gave up after 6 attempts)" seen in #132.

Fix: prewarmCaBundle() loads the bundle single-threaded right after each
std.http.Client is created (main.zig session + oneshot paths), setting
client.now so every later connect — including concurrent subagents — skips the
rescan entirely and just reads the stable, immutable bundle. No race window.

Verified: zig build; zig build test 152/152; a normal HTTPS turn still works
(pre-warm is transparent).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@justrach
justrach merged commit 819a55b into main Jul 10, 2026
6 checks passed
@justrach
justrach deleted the fix/tls-ca-bundle-race-131 branch July 15, 2026 10:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

GPT-5.5 subagent calls fail with ms=0 / resp_bytes=0 — model crashes immediately

1 participant