grpc/grpc-go · error

external processor returned unexpected status

Error message

external processor returned unexpected status %v for body response, expected %v

What it means

Raised by validateBodyResponse (ext_proc.go:1524) when a request_body or response_body response from the server has a status other than CONTINUE. Body responses must use CommonResponse.CONTINUE; any other status is a protocol violation. failProcStream fails the RPC unless failure_mode_allow bypasses it.

Solutions

  1. On the server, always set status = CommonResponse.CONTINUE on body responses.
  2. If the server needs to abort the RPC, use the immediate_response field with a proper gRPC status instead.
  3. Enable failure_mode_allow so the client tolerates the bad status and bypasses ext_proc.
  4. Separate the header and body response construction so status logic is not copied across paths.

Example fix

// before: body response carries a non-CONTINUE status
&procpb.BodyResponse{Response: &procpb.CommonResponse{Status: procpb.CommonResponse_RESET, BodyMutation: mut}}

// after: body responses must use CONTINUE
&procpb.BodyResponse{Response: &procpb.CommonResponse{Status: procpb.CommonResponse_CONTINUE, BodyMutation: mut}}
Defensive patterns

Strategy: fallback

Validate before calling

// On the ext_proc SERVER: body responses must always carry CONTINUE.
func buildBodyResponse(mut *procpb.BodyMutation) *procpb.BodyResponse {
    return &procpb.BodyResponse{Response: &procpb.CommonResponse{
        Status: procpb.CommonResponse_CONTINUE,
        BodyMutation: mut,
    }}
}

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 body response") {
    // server returned a non-CONTINUE status on a body response
}

Prevention

When it happens

Trigger: Triggered when bodyResp.GetResponse().GetStatus() (ext_proc.go:1523) != CONTINUE on a body message the client received from the server.

Common situations: Server attempts to reset/replace a body via status on the body response (not supported here), reuses header-status logic on the body path, or returns an error status instead of failing the RPC explicitly.

Related errors


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

Appendix: source

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

			if cs.responseTrailerReady.HasFired() {
				cs.failProcStream(fmt.Errorf("external processor unexpectedly sent duplicate response trailers after response trailers were already processed"))
				return
			}
			trailer := resp.GetResponseTrailers()
			if err = cs.applyMutations(trailer.GetHeaderMutation(), cs.responseTrailers); err != nil {
				cs.failProcStream(err)
				return
			}
			// Signal that the response trailer is modified and ready to be sent to
			// the client.
			cs.fireResponseTrailerReady()
		}
	}
}

func (cs *clientStream) validateBodyResponse(bodyResp *v3procservicepb.BodyResponse) (*v3procservicepb.StreamedBodyResponse, bool) {
	if status := bodyResp.GetResponse().GetStatus(); status != v3procservicepb.CommonResponse_CONTINUE {
		cs.failProcStream(fmt.Errorf("external processor returned unexpected status %v for body response, expected %v", status, v3procservicepb.CommonResponse_CONTINUE))
		return nil, false
	}
	streamedResp := bodyResp.GetResponse().GetBodyMutation().GetStreamedResponse()
	if streamedResp == nil {
		cs.failProcStream(fmt.Errorf("external processor returned invalid body mutation in body response"))
		return nil, false
	}
	if streamedResp.GetGrpcMessageCompressed() {
		cs.failProcStream(fmt.Errorf("external processor returned compressed grpc message which is not supported"))
		return nil, false
	}
	return streamedResp, true
}

func (cs *clientStream) applyMutations(mutation *v3procservicepb.HeaderMutation, md metadata.MD) error {
	if mutation == nil {
		return nil
	}

View on GitHub (pinned to 0c51461d27)