An Alloy user had one header configured on pyroscope.scrape, an X-Api-Key. About two hours in, profiling stopped. Their packet capture showed a single GET carrying roughly 1,000 copies of that header, and the target answering 431 Request Header Fields Too Large.

The bug was in prometheus/common, the library behind http_config in Prometheus, Alertmanager and the exporters. The reporter had already traced it to cloneRequest. I wrote the upstream fix, #982, which shipped in v0.72.1.

Reproducing it

One header in the config, the same request sent four times:

$ go run . # prometheus/common v0.71.0 server saw X-Api-Key x1 server saw X-Api-Key x2 server saw X-Api-Key x3 server saw X-Api-Key x4 $ go run . # prometheus/common v0.72.1 server saw X-Api-Key x1 server saw X-Api-Key x1 server saw X-Api-Key x1 server saw X-Api-Key x1 

The same request sent four times: on v0.71.0 the header count grows from 1 to 4, on v0.72.1 it stays at 1

The copy that copied nothing

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

The comment says deep copy. It isn't one. *r2 = *r leaves both requests pointing at the same header map, and maps.Copy copies that map into itself. Anything the library adds afterwards lands in the caller's request. http_headers uses Header.Add, so a request that gets reused picks up one more copy per send.

A struct copy shares the header map; Header.Clone gives the clone its own map

Checking your binaries

$ go version -m ./alloy | grep prometheus/common  dep github.com/prometheus/common v0.71.0 h1:... 

v0.69.0 to v0.72.0 are affected. v0.72.1 or later is fixed. You only hit it with an affected version, an http_headers block, and code that sends the same request object twice.

Prometheus 3.15 pins the same broken version and never shows the bug, because an otelhttp wrapper clones each request first. The full post has the exposure table for five tools and the regression test that went in with the fix.

Read the full post on saipisey.com →