grpc/grpc-go · error

external processor unexpectedly sent response headers when r

Error message

external processor unexpectedly sent response headers when response header processing is disabled

What it means

Raised when the server sends a response_headers response while responseHeaderMode is modeSkip. The client never asked to process response headers, so a response-headers mutation is a protocol violation and the proc stream is failed.

Source

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

			// response body message, fail the RPC.
			if cs.config.processingModes.responseTrailerMode == modeSend && cs.responseTrailerReady.HasFired() {
				cs.failProcStream(fmt.Errorf("external processor sent response body after response trailers were already processed"))
				return
			}

			streamedResp, ok := cs.validateBodyResponse(resp.GetResponseBody())
			if !ok {
				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
			}

View on GitHub (pinned to 03255a9237)

Solutions

  1. Set processing_mode.response_header_mode to SEND if you want header processing.
  2. Otherwise fix the server to not emit response_headers when the negotiated ProtocolConfiguration implies headers are skipped.
  3. Enable failure_mode_allow to bypass while fixing.

Example fix

// before
processing_mode:
  response_header_mode: SKIP   # server sends response_headers anyway

// after
processing_mode:
  response_header_mode: SEND
Defensive patterns

Strategy: validation

Validate before calling

// Server: suppress response_headers when the negotiated header mode is SKIP.
func maybeResponseHeaders(mode v3procfilterpb.ProcessingMode_HeaderSendMode, resp *pb.ProcessingResponse) *pb.ProcessingResponse {
    if mode == v3procfilterpb.ProcessingMode_SKIP && resp.GetResponseHeaders() != nil {
        return nil
    }
    return resp
}

Try / catch

// Client: failure_mode_allow -> bypass.

Prevention

When it happens

Trigger: processing_mode.response_header_mode == SKIP (or defaulted to SKIP via per-route override) and the server emits a ProcessingResponse.response_headers.

Common situations: Server always acks response headers regardless of mode. Per-route override disabled header processing for a route but the server is shared and assumes headers are on.

Related errors


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