From c5e0930658b848f3ff039a7fb9f9d4beaeab5330 Mon Sep 17 00:00:00 2001 From: mhx Date: Thu, 3 Sep 2026 09:16:19 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20restore=20the=20drain=20that=20a?= =?UTF-8?q?=20stray=20checkout=20reverted?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Commit 7eedf65 accidentally reverted bouncer.go: a scratch copy of this worktree kept a .git file pointing back here, so a `git checkout origin/main -- bouncer.go` run inside the copy wrote the revert into this index, and the next commit carried it. The branch was left with the regression test but not the fix, which is exactly what CI reported -- 10 connections for 10 calls. Restores the drain, the dead-branch removal and MaxIdleConnsPerHost. Re-verified against all three CI gates in containers: golangci-lint v1.63.4 clean, go test -cover green, yaegi v0.16.1 PASS. --- bouncer.go | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/bouncer.go b/bouncer.go index 5f02e27..379a4e3 100644 --- a/bouncer.go +++ b/bouncer.go @@ -243,17 +243,19 @@ func New(_ context.Context, next http.Handler, config *configuration.Config, nam }, httpClient: &http.Client{ Transport: &http.Transport{ - MaxIdleConns: 10, - IdleConnTimeout: 30 * time.Second, - TLSClientConfig: tlsConfig, + MaxIdleConns: 10, + MaxIdleConnsPerHost: 10, + IdleConnTimeout: 30 * time.Second, + TLSClientConfig: tlsConfig, }, Timeout: time.Duration(config.HTTPTimeoutSeconds) * time.Second, }, httpAppsecClient: &http.Client{ Transport: &http.Transport{ - MaxIdleConns: 10, - IdleConnTimeout: 30 * time.Second, - TLSClientConfig: tlsAppsecConfig, + MaxIdleConns: 10, + MaxIdleConnsPerHost: 10, + IdleConnTimeout: 30 * time.Second, + TLSClientConfig: tlsAppsecConfig, }, Timeout: time.Duration(config.HTTPTimeoutSeconds) * time.Second, }, @@ -278,7 +280,7 @@ func New(_ context.Context, next http.Handler, config *configuration.Config, nam log, bouncer.cacheClient, &http.Client{ - Transport: &http.Transport{MaxIdleConns: 10, IdleConnTimeout: 30 * time.Second}, + Transport: &http.Transport{MaxIdleConns: 10, MaxIdleConnsPerHost: 10, IdleConnTimeout: 30 * time.Second}, Timeout: time.Duration(config.HTTPTimeoutSeconds) * time.Second, }, config.CaptchaProvider, @@ -801,6 +803,13 @@ func appsecQuery(bouncer *Bouncer, ip string, httpReq *http.Request) error { return nil } defer func() { + // net/http only returns a connection to the idle pool once its body has + // been read to EOF; closing early discards it. Drain here rather than at + // the end of the function so the 500 and non-200 paths, which return + // earlier, keep their connections too. + if _, errDrain := io.Copy(io.Discard, res.Body); errDrain != nil { + bouncer.log.Debug("appsecQuery:drainBody " + errDrain.Error()) + } if err = res.Body.Close(); err != nil { bouncer.log.Error("appsecQuery:closeBody " + err.Error()) } @@ -816,9 +825,6 @@ func appsecQuery(bouncer *Bouncer, ip string, httpReq *http.Request) error { return fmt.Errorf("appsecQuery statusCode:%d", res.StatusCode) } - if err != nil { - return fmt.Errorf("appsecQuery:readBody %w", err) - } return nil }