grpc/grpc-go · error

external processor unexpectedly set end of stream in respons

Error message

external processor unexpectedly set end of stream in response body mutation

What it means

Raised when the server's streamed response-body mutation sets EndOfStream=true. End of stream on the dataplane is driven by the dataplane itself, not by the ext-proc server, so a server-supplied EOS inside a streamed response body is treated as a protocol violation and fails the proc stream.

Source

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

			// RPC.
			if cs.config.processingModes.responseHeaderMode == modeSend && !cs.responseHeadersReady.HasFired() {
				cs.failProcStream(fmt.Errorf("external processor sent response body before sending response headers"))
				return
			}

			// If mutated response trailers have been received before receiving the
			// 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
			}

View on GitHub (pinned to 03255a9237)

Solutions

  1. Stop setting end_of_stream on streamed_response messages from the server.
  2. Let the dataplane drive stream completion; the server should simply stop sending body messages when done.
  3. Enable failure_mode_allow to bypass during remediation.

Example fix

// before (server)
stream.Send(&pb.ProcessingResponse{ResponseBody:&pb.BodyResponse{Response:&pb.CommonResponse{BodyMutation:&pb.BodyMutation{BodyMutation:&pb.BodyMutation_StreamedResponse{StreamedResponse:&pb.StreamedBodyResponse{Body: chunk, EndOfStream: true}}}}}})

// after
stream.Send(&pb.ProcessingResponse{ResponseBody:&pb.BodyResponse{Response:&pb.CommonResponse{BodyMutation:&pb.BodyMutation{BodyMutation:&pb.BodyMutation_StreamedResponse{StreamedResponse:&pb.StreamedBodyResponse{Body: chunk}}}}}})
Defensive patterns

Strategy: validation

Validate before calling

// Server: never set EndOfStream on streamed responses.
resp := &pb.StreamedBodyResponse{Body: chunk} // EndOfStream left false

Try / catch

// Client: failure_mode_allow -> bypass.

Prevention

When it happens

Trigger: validateBodyResponse receives a StreamedBodyResponse with EndOfStream set to true from the server's response_body.body_mutation.streamed_response.

Common situations: Server copied an Envoy pattern (where EOS in body mutation is allowed) into a gRPC-extproc implementation without reading the gRPC contract. Server attempts to signal end-of-response via EOS.

Related errors


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