grpc/grpc-go · error

external processor unexpectedly sent response headers when…

Error message

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

What it means

Raised by recvFromProcServerLoop (ext_proc.go:1469) when responseHeaderMode is modeSkip but the ext_proc server sends a response_headers response. Mutating response headers the client was told not to send is a protocol violation; failProcStream fails the RPC unless failure_mode_allow bypasses it.

Solutions

  1. On the server, only return response_headers when the client's ProtocolConfiguration indicates response header processing is enabled.
  2. If response header mutation is desired, set response_header_mode to SEND in the xDS config.
  3. Enable failure_mode_allow so the client tolerates the violation and proceeds to the dataplane.
  4. Make the server handler mode-aware by reading the negotiated ProtocolConfiguration on stream start.

Example fix

// before: server always sends response header mutation
return &procpb.ProcessingResponse{Response: &procpb.ProcessingResponse_ResponseHeaders{...}}, nil

// after: only when response header mode is enabled on the client
if respHeaderModeEnabled {
  return &procpb.ProcessingResponse{Response: &procpb.ProcessingResponse_ResponseHeaders{...}}, nil
}
Defensive patterns

Strategy: fallback

Validate before calling

// On the ext_proc SERVER: gate response_headers output on negotiated mode.
func shouldEmitResponseHeaders(protocolCfg *procpb.ProtocolConfiguration) bool {
    // The client advertises response header processing via the absence/presence
    // negotiated out-of-band; mirror the xDS response_header_mode.
    return responseHeaderModeEnabled // derived from your config + the client's protocol config
}

Try / catch

filter.failure_mode_allow = true
if st, ok := status.FromError(err); ok && st.Code() == codes.Internal &&
    strings.Contains(st.Message(), "unexpectedly sent response headers") {
    // server returned response_headers while client mode is SKIP
}

Prevention

When it happens

Trigger: Triggered when response_header_mode is SKIP and the server returns a ProcessingResponse with response_headers set (ext_proc.go:1467).

Common situations: Server assumes response header processing is always on, a templated handler that always returns response header mutations, or config drift between server expectations and the xDS processing_mode.

Related errors


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

Appendix: source

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

			// 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 0c51461d27)