grpc/grpc-go · error

external processor returned invalid body mutation in body re

Error message

external processor returned invalid body mutation in body response

What it means

validateBodyResponse extracts the streamed body from the BodyMutation in the body response (ext_proc.go:1361). If BodyMutation or its StreamedResponse field is nil, the filter has no valid chunk to apply and fails the proc stream.

Source

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

			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
	}
	if err := cs.config.mutationRules.ApplyAdditions(mutation.GetSetHeaders(), md); err != nil {
		return err
	}
	return cs.config.mutationRules.ApplyRemovals(mutation.GetRemoveHeaders(), md)
}

View on GitHub (pinned to 03255a9237)

Solutions

  1. On the extproc server, always populate bodyMutation.streamed_response (a StreamedBodyResponse with the chunk to forward) when sending a body response.
  2. If the goal is no-op, still echo the received body bytes in StreamedResponse rather than sending an empty mutation.
  3. Confirm the server's protobuf definitions match go-control-plane's envoy.service.ext_proc.v3.

Example fix

// before: body mutation left empty
resp.Response.BodyMutation = &procservicepb.BodyMutation{}

// after: populate streamed response
resp.Response.BodyMutation = &procservicepb.BodyMutation{
    BodyMutation: &procservicepb.BodyMutation_StreamedResponse{
        StreamedResponse: &procservicepb.StreamedBodyResponse{Chunk: chunk},
    },
}
Defensive patterns

Strategy: validation

Validate before calling

// Server side: ensure StreamedResponse is populated before sending
if bodyResp.GetResponse().GetBodyMutation().GetStreamedResponse() == nil {
    bodyResp.Response.BodyMutation = &procservicepb.BodyMutation{BodyMutation: &procservicepb.BodyMutation_StreamedResponse{StreamedResponse: &procservicepb.StreamedBodyResponse{Chunk: chunk}}}
}

Prevention

When it happens

Trigger: The extproc server sends a body ProcessingResponse where BodyMutation is absent or does not populate the StreamedResponse oneof (e.g. it left BodyMutation at its zero value or set the wrong oneof).

Common situations: Server intends to pass the body through unchanged but constructs an empty BodyMutation; server uses the deprecated/cleared mutation variant; protobuf field rename/oneof mismatch between server and client.

Related errors


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