Description
When a URL registered with Cache.Register points to an unreachable host (e.g. http://127.0.0.1:1), the worker goroutines spawned by Client.Start block indefinitely. Cancelling the context passed to NewCache/jwk.NewCache does not terminate these workers.
Reproduction
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
cache, _ := jwk.NewCache(ctx, httprc.NewClient())
cache.Register(ctx, "http://127.0.0.1:1/jwks.json",
jwk.WithHTTPClient(&http.Client{Timeout: 10 * time.Second}),
)
// ctx expires after 5s, but httprc workers never stop.
// The process hangs until killed.
In a Go test binary, this causes panic: test timed out even though the test function itself returns immediately.
Root cause
-
Fetch calls are not wrapped with the parent context. Workers in worker.go:Run call the HTTP client without attaching the worker/parent context to the request. A bounded http.Client.Timeout helps for individual requests, but httprc retries immediately on failure — so the workers loop forever.
-
No backoff or retry cap on connection failures. A connection-refused error triggers an immediate retry with no backoff and no max-retry limit, creating a tight loop that only stops when the context is cancelled — which brings us back to point 1.
Expected behavior
Environment
- httprc v3.0.5
- jwx v3.0.13
- Go 1.26.2 / darwin arm64
Description
When a URL registered with
Cache.Registerpoints to an unreachable host (e.g.http://127.0.0.1:1), the worker goroutines spawned byClient.Startblock indefinitely. Cancelling the context passed toNewCache/jwk.NewCachedoes not terminate these workers.Reproduction
In a Go test binary, this causes
panic: test timed outeven though the test function itself returns immediately.Root cause
Fetch calls are not wrapped with the parent context. Workers in
worker.go:Runcall the HTTP client without attaching the worker/parent context to the request. A boundedhttp.Client.Timeouthelps for individual requests, but httprc retries immediately on failure — so the workers loop forever.No backoff or retry cap on connection failures. A connection-refused error triggers an immediate retry with no backoff and no max-retry limit, creating a tight loop that only stops when the context is cancelled — which brings us back to point 1.
Expected behavior
req.WithContext(workerCtx)so that parent context cancellation aborts in-flight HTTP requests.Environment