grpc/grpc-go · error
external processor unexpectedly sent duplicate response…
Error message
external processor unexpectedly sent duplicate response headers after response headers were already processed
What it means
Raised by recvFromProcServerLoop (ext_proc.go:1477) when the ext_proc server sends a second response_headers response after response headers were already processed (responseHeadersReady already fired). Duplicate header mutations are not allowed; failProcStream fails the RPC unless failure_mode_allow bypasses it.
Solutions
- On the server, send response_headers at most once per stream.
- Track per-stream state in the handler so the header path is not re-entered.
- Enable failure_mode_allow so a duplicate does not fail the user RPC.
- Add server-side unit tests asserting response_headers is sent exactly once.
Example fix
// before: header mutation sent on every received message
for { req, _ := stream.Recv(); stream.Send(respHeaders(req)) }
// after: send once, then only other phases
headersSent := false
for {
req, _ := stream.Recv()
if _, ok := req.Request.(*procpb.ProcessingRequest_ResponseHeaders); ok && !headersSent {
stream.Send(respHeaders(req)); headersSent = true
}
} Defensive patterns
Strategy: fallback
Validate before calling
// On the ext_proc SERVER: ensure response_headers is sent at most once.
type streamState struct{ respHeadersSent bool }
func (s *streamState) allowRespHeaders() bool {
if s.respHeadersSent { return false }
s.respHeadersSent = true
return true
} Try / catch
filter.failure_mode_allow = true
if st, ok := status.FromError(err); ok && st.Code() == codes.Internal &&
strings.Contains(st.Message(), "duplicate response headers") {
// server sent response_headers more than once
} Prevention
- Server: send response_headers exactly once per stream.
- Guard the response-header code path with per-stream state.
- Enable failure_mode_allow so duplicates degrade rather than fail.
- Server unit test: assert response_headers count == 1 across a full stream.
When it happens
Trigger: Triggered when the server sends response_headers (ext_proc.go:1467) a second time on a stream where responseHeadersReady.HasFired() is already true.
Common situations: Server handler that re-enters its response-header code path (e.g. on each received message), an unguarded loop sending header mutations more than once, or a buggy fan-out handler duplicating responses.
Related errors
- external processor returned unexpected status
- external processor sent response body before sending…
- external processor sent response headers before response…
- external processor unexpectedly sent duplicate response…
- external processor unexpectedly sent response headers when…
AI-assisted analysis of grpc/grpc-go@0c51461d27 (2026-08-11).
Data as JSON: /api/errors/587324ff759bea72.
Report an issue: GitHub.
Appendix: source
Thrown at internal/xds/httpfilter/extproc/ext_proc.go:1477
return
}
if streamedResp.GetEndOfStream() {
cs.failProcStream(fmt.Errorf("external processor unexpectedly set end of stream in response body mutation"))
return
}
cs.mutatedRespBuffer.Put(streamedResp)
case resp.GetResponseHeaders() != nil:
if cs.config.processingModes.responseHeaderMode == modeSkip {
cs.failProcStream(fmt.Errorf("external processor unexpectedly sent response headers when response header processing is disabled"))
return
}
if !cs.responseHeaderSent.Load() {
cs.failProcStream(fmt.Errorf("external processor sent response headers before response headers were sent to it"))
return
}
if cs.responseHeadersReady.HasFired() {
cs.failProcStream(fmt.Errorf("external processor unexpectedly sent duplicate response headers after response headers were already processed"))
return
}
header := resp.GetResponseHeaders()
// Check if the status in the header response is CONTINUE; if not, fail
// the stream.
if status := header.GetResponse().GetStatus(); status != v3procservicepb.CommonResponse_CONTINUE {
cs.failProcStream(fmt.Errorf("external processor returned unexpected status %v for response headers, expected %v", status, v3procservicepb.CommonResponse_CONTINUE))
return
}
if err = cs.applyMutations(header.GetResponse().GetHeaderMutation(), cs.responseHeader); err != nil {
cs.failProcStream(err)
return
}
// Signal that the response header is modified and ready to be sent to the
// client, so that if there is any buffered response body, it can be sent
// after the header.
cs.fireResponseHeadersReady()View on GitHub (pinned to 0c51461d27)