Repository navigation
Description
What version of Knative?
knative version:1.17.1
Observed in a downstream build based on Knative Serving. The queue-proxy logs report:
"commit":"20c92a1-dirty"The downstream commit resolves to 20c92a10d2ff6aa0162e0c74f9080e1fac7ec441. The relevant timeout handler code matches current upstream main in the suspected critical path: timeoutWriter.Header() directly returns the underlying http.ResponseWriter header map without synchronization.
I also checked upstream main at dbce551; pkg/http/handler/timeout.go still contains:
func (tw *timeoutWriter) Header() http.Header { return tw.w.Header() }Expected Behavior
queue-proxy should not crash due to concurrent HTTP/2 response header/trailer access. Request cancellation or timeout should fail only the affected request, not the queue-proxy process.
Actual Behavior
queue-proxy crashed and restarted with exitCode=2. The fatal runtime error was:
fatal error: concurrent map iteration and map write
The panic stack points to HTTP/2 response finalization iterating/promoting trailers while the response header map was concurrently modified:
fatal error: concurrent map iteration and map write
goroutine 1123810148 [running]:
internal/runtime/maps.fatal({0x128031e?, 0xc000524530?})
runtime/panic.go:1058 +0x18
internal/runtime/maps.(*Iter).Next(0xc000948c20?)
internal/runtime/maps/table.go:683 +0x86
golang.org/x/net/http2.(*responseWriterState).promoteUndeclaredTrailers(0xc001d67780)
golang.org/x/net@v0.34.0/http2/server.go:2851 +0x65
golang.org/x/net/http2.(*responseWriterState).writeChunk(0xc001d67780, {0xc0008aa000, 0xf, 0x1000})
golang.org/x/net@v0.34.0/http2/server.go:2717 +0x77
golang.org/x/net/http2.chunkWriter.Write({0x10610e0?}, {0xc0008aa000?, 0xc0012831e0?, 0x1ce0680?})
golang.org/x/net@v0.34.0/http2/server.go:2670 +0x1d
bufio.(*Writer).Flush(0xc000b093c0)
bufio/bufio.go:643 +0x55
golang.org/x/net/http2.(*responseWriter).FlushError(0x142e0a0?)
golang.org/x/net@v0.34.0/http2/server.go:2935 +0x31
golang.org/x/net/http2.(*responseWriter).Flush(...)
golang.org/x/net@v0.34.0/http2/server.go:2925
golang.org/x/net/http2.(*responseWriter).handlerDone(0xc001425a50)
golang.org/x/net@v0.34.0/http2/server.go:3100 +0x25
golang.org/x/net/http2.(*serverConn).runHandler.func1()
golang.org/x/net@v0.34.0/http2/server.go:2473 +0x205
golang.org/x/net/http2.(*serverConn).runHandler(0x44cf12?, 0x0?, 0x0?, 0xc000146fb8?)
golang.org/x/net@v0.34.0/http2/server.go:2477 +0x109
created by golang.org/x/net/http2.(*serverConn).scheduleHandler in goroutine 624989236
golang.org/x/net@v0.34.0/http2/server.go:2409 +0x21d
At the same timestamp, other goroutines were in the queue-proxy timeout / reverse-proxy handler path:
knative.dev/pkg/network.ErrorHandler.func1
knative.dev/pkg@v0.0.0-20250117084104-c43477f0052b/network/error_handler.go:33
net/http/httputil.(*ReverseProxy).ServeHTTP
net/http/httputil/reverseproxy.go:486
knative.dev/serving/pkg/queue.(*appRequestMetricsHandler).ServeHTTP
knative.dev/serving/pkg/queue/request_metric.go:205
knative.dev/serving/pkg/queue/sharedmain.mainHandler.ProxyHandler.func3
knative.dev/serving/pkg/queue/handler.go:76
net/http.HandlerFunc.ServeHTTP
net/http/server.go:2294
knative.dev/serving/pkg/queue/sharedmain.mainHandler.ForwardedShimHandler.func4
knative.dev/serving/pkg/queue/forwarded_shim.go:54
net/http.HandlerFunc.ServeHTTP
net/http/server.go:2294
knative.dev/serving/pkg/http/handler.(*timeoutHandler).ServeHTTP.func4
knative.dev/serving/pkg/http/handler/timeout.go:118
Steps to Reproduce the Problem
I do not yet have a minimal standalone reproducer. The observed production trigger appears to involve:
- queue-proxy serving HTTP/2 traffic.
- Concurrent proxied requests.
- Request cancellation or timeout while response handling is being finalized.
- A response header/trailer map is modified while the HTTP/2 server iterates/promotes trailers during
handlerDone.
Suspected Cause
The crash looks like concurrent access to the underlying http.Header map owned by the HTTP/2 response writer.
timeoutWriter synchronizes Flush, Write, WriteHeader, and timeout error writes with tw.mu, but Header() returns the raw underlying header map without synchronization:
func (tw *timeoutWriter) Header() http.Header { return tw.w.Header() }Because callers can mutate the returned map outside timeoutWriter's mutex, the HTTP/2 server may concurrently iterate the same map in promoteUndeclaredTrailers, causing:
fatal error: concurrent map iteration and map write
This may be related to or another manifestation of #15850.
Activity
shashankvarma499 commentedon Sep 13, 2026
Thanks for the detailed report and the pinned stack trace. I traced the suspected path and I agree the race is real, but the right fix has a couple of valid shapes, so I wanted to lay out the cause and a proposed direction before sending a PR.
Root cause
timeoutWriter.Header()inpkg/http/handler/timeout.goreturns the underlying writer's map without takingtw.mu:Every mutating method (
Write,WriteHeader,Flush, and thetry*helpers) holdstw.mu, butHeader()hands the rawhttp.Headermap back to the caller. A caller on the handler goroutine can then dotw.Header().Set(...)while the HTTP/2 server's writer goroutine iterates the same map inpromoteUndeclaredTrailersduringwriteChunk/handlerDone. That is the "concurrent map iteration and map write" in your stack.There is a second aggravating factor in
timeoutHandler.ServeHTTP: on the timeout branch it returns as soon astryTimeoutAndWriteErrorsucceeds, without waiting for the handler goroutine to finish. The inner handler keeps running (and can keep mutating headers / writing) afterServeHTTPhas already returned and the HTTP/2 server has begun finalizing the response. ThetimedOutflag already makes post-timeoutWrite/WriteHeader/Flushno-ops, butHeader()bypasses that guard entirely, which is why header mutation is the path that still races.Proposed direction
The challenge is that simply locking
tw.muaround theHeader()return does not help, because the caller mutates the returned map after the method returns, and the HTTP/2 server iterates the underlying map directly rather than through the wrapper. Two directions seem viable:timedOuthandling forWrite/WriteHeader/Flush. That meansHeader()returns a header view that refuses (or ignores) mutations oncetw.timedOutis set. This is the smallest change and preserves the current abandon-the-handler-on-timeout design.ServeHTTPreturns on the timeout path (wait fordone). This removes the concurrent access entirely but changes timeout semantics and could block the timeout path, so it is a larger behavioural change.I lean toward (1) as it reuses the existing
timedOutmechanism rather than introducing a new synchronization scheme, but (2) is arguably the more complete fix.Tests
go test -raceunit test inpkg/http/handlerthat exercises a handler which callsHeader().Setafter a timeout while the response is being finalized, asserting the write is dropped and no race is reported.Header().Setpath is unchanged.Defer
If this is the same underlying issue as #15850, or if there is already an in-flight fix, I will leave it alone. Otherwise I can put up a PR for direction (1) once the approach is confirmed.