fix: hold idle HTTP connections open past the OS proxy's reuse window; 33.0.8:1 → 33.0.8:2 - #125
Conversation
…; 33.0.8:1 -> 33.0.8:2 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
MattDHill
left a comment
There was a problem hiding this comment.
Verified the diagnosis independently rather than taking the issue's word for it, and every load-bearing claim holds.
Proxy side (start-technologies @ 8a916a0da): run_http1_proxy handshakes once on the single upstream stream and shares it behind Arc<Mutex> (net/http.rs:330) — nothing re-dials; header_read_timeout is a hardcoded Duration::from_secs(60) (net/http.rs:337), so the 60 s figure is stable; try_join(from.with_upgrades(), to.with_upgrades()) (net/http.rs:412) means the upstream future resolving Ok on Apache's FIN leaves the client leg running; the failed send_request returns Err out of the service_fn (net/http.rs:368) so hyper aborts with no response written — precisely RemoteHostClosedError. Eager dial confirmed at net/vhost.rs:1096 (inside preprocess, the ClientHello hook), and http_aware selection at net/vhost.rs:1206/1226. The http preset sets addXForwardedHeaders: true and pins ALPN http/1.1 (Host.ts:23-29), and interfaces.ts binds protocol: 'http', so this package is always on the affected h1 path. HOST_IP = [10,0,3,1] (lib.rs:8) matches the log signature.
Apache side, measured in the image (nextcloud:32.0.6-apache — same Debian/php base): apache2.conf:111 sets KeepAliveTimeout 5 and IncludeOptional conf-enabled/*.conf is at apache2.conf:222, so the snippet parses last and wins; mods-enabled/*.conf is included at line 147, so header=90 overrides header=20-40,minrate=500 while body=10,minrate=500 survives untouched. Running the exact printf + a2enconf: syntax OK, reuse after 12 s idle goes FAILED → OK, and a stalled request goes closed-at-20.0 s → still open at 45 s. The PR's table reproduces. Only /var/www/html and the postgres dir are volume-mounted, so nothing shadows the baked config.
Ordering is right — 75 > 60 > 30 — and raising is the only direction that works; KeepAlive Off would be strictly worse, since the single pinned upstream would die after one request.
One thing worth recording in #3731: MaxKeepAliveRequests 100 is untouched and this change is what makes it reachable, since connections now live minutes instead of 5 s. It does not reopen the flap — Apache emits Connection: close on the 100th response, hyper's client preserves the header (h1/role.rs:1109-1130) and hyper's server honors it on encode (role.rs:821-830), so both legs close in-band.
Approving. The unverified part is exactly the part the description flags as unverified.
Fixes the periodic Connected → "Network error" → reconnect cycle reported with Nextcloud Desktop (Windows and Linux), which correlates with paired
10.0.3.1 - - "-" 408 0 "-" "-"entries in the service log. Sync was never at risk — the flap is cosmetic — but it fires on essentially every idle cycle of the desktop client's 30 s connection validator.Why it happens
The StartOS reverse proxy pins each client connection to a single backend connection (dialed at TLS ClientHello, never re-dialed) and holds idle client connections for up to 60 s, while stock Debian Apache closes its side after 5 s of keep-alive idle and 408s request-less connections after ~20 s (
mod_reqtimeout). So the backend leg dies first, the proxy keeps the client leg looking healthy, and the client's next poll aborts mid-request — surfacing asQNetworkReply::RemoteHostClosedError. The 408 pairs are the desktop client's speculative spare sockets being reaped bymod_reqtimeoutthrough the proxy's eager dial. Full analysis with code pointers and repros: Start9Labs/start-technologies#3731.The proper fix is in the OS proxy (per-request upstream dialing, tracked in that issue). This PR is the package-side hardening that removes the user-visible symptom now, and it independently also fixes the
.onionleg, which bypasses the proxy (tor dials the container directly) and hits the same 5 s close raw.The change
A conf snippet baked into the image (
startos-proxy-keepalive.conf):MaxRequestWorkers 150) this is negligible.Also bumps
33.0.8:1 → 33.0.8:2with release notes (all five locales) and documents the tuning and its diagnosis in the README's network section.Verification
Empirical, against
nextcloud:33.0.8-apachecontainers (stock vs. this snippet, via the exactprintf/a2enconfthe Dockerfile runs):Keep-Aliveadvertisedtimeout=5, max=100timeout=75, max=100GETthen silence)"-" 408 0 "-" "-"npm run check(tsc) passes. Not verified: an install on a live StartOS box (none available from this session) — the meaningful acceptance check after merge/sideload is: desktop client stays Connected across idle periods, and"-" 408entries stop appearing in the service log during normal client use.Note for the merger: pushing this to
mastertriggers tagAndRelease →v33.0.8_2publishes to the registry. Merging is releasing.🤖 Generated with Claude Code