Add configurable http_timeout so a hung upstream can't block sshd auth (closes #31) - #37
Merged
Conversation
Closes #31. NewHTTPClient previously constructed &http.Client{} with no Timeout. A misbehaving upstream (DNS that resolves but never responds, TLS handshake stall, slowloris-style server, etc.) would block the per-source goroutine until the OS-default TCP timeout - typically several minutes. cmd.go:Run waits on every per-source goroutine via wg.Wait() before closing the keyChan, so one stalled source stalls the entire login. sshd then waits on ussher's stdout for that whole duration. PR #10 made HTTP errors non-fatal but doesn't help when the request never returns at all. http.Client.Timeout now defaults to 10s and is overridable via a new http_timeout YAML field that mirrors cache_ttl: parsed by time.ParseDuration ("500ms", "10s", "30s"); empty or unparseable falls back to the default with a log message. The default trades a modest "slow but healthy upstream gave up" risk for tight bounds on auth-path latency under partial outages, which is the right trade for a tool sshd consults synchronously on every login. A timeout firing produces the same error class PR #10 already covers: GetURL logs "Failed to fetch ...: context deadline exceeded ..." and returns an empty slice, so the failing source quietly contributes zero keys instead of taking the whole invocation down. Tests cover both the wiring (NewHTTPClient propagates the timeout to http.Client.Timeout) and the behavior (a server that stalls before responding causes GetURL to return empty within ~timeout, not the OS-default). Both run under -race; the suite stays clean. README grows a parallel http_timeout section explaining the latency vs. healthy-upstream-tolerance trade-off, mirroring the cache_ttl section's framing. CHANGELOG gains Added and Fixed bullets. Out of scope: per-source http_timeout (mirrors #9 for cache_ttl; worth a sibling design if/when that lands), and the stale-on-error fallback from #11 (which would naturally cover the timeout-fired case alongside other fetch failures - good follow-up).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #31.
Summary
Adds a global
http_timeoutconfig field defaulting to10sand propagates it tohttp.Client.Timeoutso that a hung or stalled upstream can no longer blocksshd's authentication path.Why
NewHTTPClientpreviously constructed&http.Client{}with noTimeout. A misbehaving upstream — DNS that resolves but never responds, TLS handshake that hangs, slowloris-style server, anyone with control over a configured source URL — would block the per-source goroutine until the OS-default TCP timeout (typically several minutes).cmd.go:Runwaits on every per-source goroutine viawg.Wait()before closing thekeyChan, so one stalled source stalls the entire login. PR #10 made HTTP errors non-fatal, but a request that never returns isn't an error — it's just hung.A timeout firing produces the same error class PR #10 already handles:
GetURLlogsFailed to fetch ...: context deadline exceeded ...and returns an empty slice. The source quietly contributes zero keys; healthy sources keep producing keys; the rest of the login proceeds normally.Behavior
http_timeout: 30shttp_timeout: garbagehttp_timeout: 0sThe default of 10s trades a modest "slow but healthy upstream gave up" risk for tight bounds on auth-path latency under partial outages. For a tool sshd consults synchronously on every login that's the right trade; operators wanting different bounds can tune it.
Diff shape
config.go: addHTTPTimeout stringfield,defaultHTTPTimeout = 10 * time.Second, andResolveHTTPTimeout()mirroringResolveCacheTTL.httpclient.go:NewHTTPClientnow takes(ttl, httpTimeout time.Duration)and setshttp.Client.Timeout.cmd.go: passesc.ResolveHTTPTimeout()through.README.md: parallelhttp_timeoutsection under Configuration, framing the latency-vs-tolerance trade-off.CHANGELOG.md: bullets in[Unreleased]under Added (the new field) and Fixed (the underlying availability bug).Tests
TestNewHTTPClient_PropagatesTimeout— wiring assertion that the constructor'shttpTimeoutlands onhttp.Client.Timeout.TestGetURL_TimeoutIsNotFatal— behavioral test using a server that stalls before responding; verifiesGetURLreturns an empty slice within ~timeout (not the OS-default), so a hung server can't block the test process either.TestResolveHTTPTimeout— table-driven: empty / unparseable fall back to default; valid500ms/30s/2mparse correctly.TestConfigLoadHTTPTimeout—http_timeout: 5sround-trips through YAML and resolves to5 * time.Second.All run under
-race(issue #16 isn't fixed yet, but I ran the suite locally withgo test -race). Coverage 39.5% → 42.1%.Out of scope (call out, don't widen)
http_timeout— mirrors thecache_ttlper-source proposal in Per-source cache_ttl with source > global > default precedence #9. Worth a sibling design if/when that lands; deliberately not in this PR per the issue's "global" scope.Test plan
./build.shgreen; coverage 42.1%.go test -race ./...green.TestGetURL_TimeoutIsNotFatalcompletes in ~100ms (the configured timeout) — no hang.https://claude.ai/code/session_013HnepY8MhhxrJJjE5ysW47
Generated by Claude Code