grpc/grpc-go · error

external processor unexpectedly sent duplicate response head

Error message

external processor unexpectedly sent duplicate response headers after response headers were already processed

What it means

Raised when the server sends response_headers a second time after responseHeadersReady has already fired. Response headers may be mutated exactly once; a duplicate response_headers message is a protocol violation and fails the proc stream.

Source

Thrown at internal/xds/httpfilter/extproc/ext_proc.go:1311

				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 03255a9237)

Solutions

  1. Ensure the server sends exactly one response_headers response per RPC.
  2. Track in the server whether headers were already acked and skip subsequent sends.
  3. Enable failure_mode_allow to bypass while remediating.

Example fix

// before: server acks headers on every receive loop
for { req := <-stream.Recv(); stream.Send(responseHeadersFor(req)) }

// after: ack headers once
headersAcked := false
for {
  req := <-stream.Recv()
  if req.GetResponseHeaders() != nil && !headersAcked {
    stream.Send(responseHeadersFor(req)); headersAcked = true
  }
}
Defensive patterns

Strategy: validation

Validate before calling

// Server: send response_headers exactly once.
respHeadersSent := false
// if req is response_headers && !respHeadersSent { Send(...); respHeadersSent = true }

Try / catch

// Client: failure_mode_allow -> bypass.

Prevention

When it happens

Trigger: The server emits two (or more) response_headers ProcessingResponse messages on the same stream.

Common situations: Server bug where a retry/loop sends response_headers twice. Server designed to 'update' headers later, which the protocol forbids.

Related errors


AI-assisted analysis of grpc/grpc-go@03255a9237 (2026-08-07). Data as JSON: /api/errors/587324ff759bea72. Report an issue: GitHub.