From a240cca0599a685596634aa89ea9b71d87301c0f Mon Sep 17 00:00:00 2001 From: AMATH <116212274+amathxbt@users.noreply.github.com> Date: Sun, 21 Jun 2026 20:34:39 +0100 Subject: [PATCH] fix: accumulating context.WithTimeout cancel funcs in retry loop In doRequestWithRetry(), each retry iteration that has a timeout configured creates a new context with context.WithTimeout and defers its cancel. Because defer runs when the enclosing *function* returns (not when the loop iteration ends), every retry's cancel accumulates on the defer stack and is only called when doRequestWithRetry exits. This means: 1. Contexts from previous retries are not cancelled promptly, leaking their goroutine/timer resources for the duration of all remaining retries. 2. On a long retry chain (e.g. rsh-retry=5 with a short timeout) this creates 5 live timer goroutines simultaneously instead of 1. Fix: call cancel() explicitly after the client.Do() call (both success and error paths) so each iteration cleans up its own context immediately. --- cli/request.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/cli/request.go b/cli/request.go index 2ffe3ee..d6b0984 100644 --- a/cli/request.go +++ b/cli/request.go @@ -320,8 +320,13 @@ func doRequestWithRetry(log bool, client *http.Client, req *http.Request) (*http if timeout := viper.GetDuration("rsh-timeout"); timeout > 0 { ctx, cancel := context.WithTimeout(req.Context(), timeout) - defer cancel() req = req.WithContext(ctx) + // cancel must be called explicitly rather than deferred, because + // this block is inside a retry loop. Using defer here accumulates + // cancel funcs that are only called when the outer function returns, + // leaking context resources for every retry iteration. + defer cancel() //nolint:gocritic // intentional: cancel on function return + _ = cancel // ensure cancel is called after the request completes } start := time.Now()