grpc/grpc-go · error
external processor returned unexpected status
Error message
external processor returned unexpected status %v for response headers, expected %v
What it means
Raised by recvFromProcServerLoop (ext_proc.go:1485) when the response_headers mutation the server sends has a status other than CONTINUE. For response headers the client only accepts CommonResponse.CONTINUE; any other status (e.g. the server trying to short-circuit) is a protocol violation. failProcStream fails the RPC unless failure_mode_allow bypasses it.
Solutions
- On the server, always set status = CommonResponse.CONTINUE on response_headers mutations.
- Use the immediate_response field (not header status) if the server needs to short-circuit the RPC.
- Enable failure_mode_allow so the client tolerates the bad status and bypasses ext_proc.
- Separate the request-header and response-header handler code paths so status logic is not reused incorrectly.
Example fix
// before: response header carries a non-CONTINUE status
stream.Send(&procpb.ProcessingResponse{Response: &procpb.ProcessingResponse_ResponseHeaders{
ResponseHeaders: &procpb.HeadersResponse{Response: &procpb.CommonResponse{Status: procpb.CommonResponse_RESET}},
}})
// after: response headers must use CONTINUE
ResponseHeaders: &procpb.HeadersResponse{Response: &procpb.CommonResponse{Status: procpb.CommonResponse_CONTINUE}} Defensive patterns
Strategy: fallback
Validate before calling
// On the ext_proc SERVER: response headers must always carry CONTINUE.
func buildResponseHeadersResponse() *procpb.HeadersResponse {
return &procpb.HeadersResponse{Response: &procpb.CommonResponse{Status: procpb.CommonResponse_CONTINUE}}
} Try / catch
filter.failure_mode_allow = true
if st, ok := status.FromError(err); ok && st.Code() == codes.Internal &&
strings.Contains(st.Message(), "unexpected status") &&
strings.Contains(st.Message(), "for response headers") {
// server returned a non-CONTINUE status on response headers
} Prevention
- Server: always set status = CONTINUE on response_headers responses.
- Use immediate_response (not header status) to short-circuit the RPC.
- Enable failure_mode_allow to tolerate the bad status.
- Keep request-header and response-header status logic separate.
When it happens
Trigger: Triggered when the server's response_headers response carries response.status != CONTINUE (ext_proc.go:1484).
Common situations: Server reuses a request-headers handler that returns RESET/REPLACE/ABSOLUTE for response headers, or attempts an immediate-response-style rejection via the header status on the response path (where only CONTINUE is valid).
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/9ad6cdf56c6a1681.
Report an issue: GitHub.
Appendix: source
Thrown at internal/xds/httpfilter/extproc/ext_proc.go:1485
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()
case resp.GetResponseTrailers() != nil:
if cs.config.processingModes.responseTrailerMode == modeSkip {
cs.failProcStream(fmt.Errorf("external processor unexpectedly sent response trailers when response trailer processing is disabled"))
return
}
if !cs.trailerSent.Load() {
cs.failProcStream(fmt.Errorf("external processor sent response trailers before response trailers were sent to it"))View on GitHub (pinned to 0c51461d27)