Skip to content

fix(config): deep copy the header in cloneRequest - #982

Open
SaiPisey2 wants to merge 3 commits into
prometheus:mainfrom
SaiPisey2:fix/clone-request-header-deep-copy
Open

fix(config): deep copy the header in cloneRequest#982
SaiPisey2 wants to merge 3 commits into
prometheus:mainfrom
SaiPisey2:fix/clone-request-header-deep-copy

Conversation

@SaiPisey2

Copy link
Copy Markdown

Fixes #981.

cloneRequest intends a deep copy of the header but does not make one:

r2 := new(http.Request)
*r2 = *r
// Deep copy of the Header.
maps.Copy(r.Header, r2.Header)

After *r2 = *r, r2.Header is already the same map as r.Header, so maps.Copy copies it onto itself and the clone shares the caller's header map.

Every round tripper here clones and then modifies, so each one mutates the request it was given. headersRoundTripper uses Header.Add, so a reused request gains another copy of every configured header on each round trip, without bound. grafana/alloy#7016 reports ~1000 identical X-Api-Key lines in a single request, with the target returning 431 and profiling stopping.

Introduced in 56870db, which replaced the manual copy loop with maps.Copy. The arguments are also the wrong way round, but swapping them is not enough — the clone needs its own map.

Header.Clone() returns nil for a nil header, while the previous code always produced a usable map, so the nil case is kept: userAgentRoundTripper and others call Header.Set straight after cloning, which panics with assignment to entry in nil map on a request built without one. That path is broken on main today as well.

Tests

Two tests, both failing on main and passing here:

  • TestCloneRequest — the clone must not share the caller's map, and a request without a header must still be usable afterwards.
  • TestHeadersRoundTripperReusedRequest — reusing a request must not accumulate headers, and must not modify the caller's request.

go build ./..., go test ./... and go test -race ./config/ are green on darwin/arm64, go1.26.5.

The shallow struct copy leaves r2.Header aliasing r.Header, so
maps.Copy(r.Header, r2.Header) copies the map onto itself and the clone
shares the caller's header map.

Every round tripper that adds a header therefore mutates the request it
was given. headersRoundTripper uses Header.Add, so a reused request
accumulates another copy of every configured header on each round trip,
without bound, until the server rejects the request.

Clone the header instead, keeping the map non-nil so round trippers that
call Header.Set on a request built without one still work.

Signed-off-by: SaiPisey2 <piseysai0202@gmail.com>
Covers both halves of the clone: the copy must not be shared with the
caller, and a request built without a header must still be usable
afterwards, which previously panicked with "assignment to entry in nil
map" on the first Header.Set.

Signed-off-by: SaiPisey2 <piseysai0202@gmail.com>
testifylint requires the f-suffixed assertions when a message is passed.

Signed-off-by: SaiPisey2 <piseysai0202@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

cloneRequest does not copy the header, so round trippers mutate the caller's request

1 participant