grpc/grpc-go · error

external processor sent response body after response trailer

Error message

external processor sent response body after response trailers were already processed

What it means

Raised when a response_body message arrives after response trailers have already been processed (responseTrailerMode == SEND and responseTrailerReady has fired). The required ordering is headers -> body -> trailers; a body after trailers is illegal and fails the proc stream.

Source

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

		case resp.GetResponseBody() != nil:
			if cs.config.processingModes.responseBodyMode == modeSkip {
				cs.failProcStream(fmt.Errorf("external processor unexpectedly sent response body when response body processing is disabled"))
				return
			}

			// If response headers have been sent and mutated response headers have
			// not been received before receiving the response body message, fail the
			// 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
			}

View on GitHub (pinned to 03255a9237)

Solutions

  1. Serialize all ProcessingResponse sends in the server and ensure body messages precede the single response_trailers message.
  2. Make sure trailers are only sent once the server has received the body EndOfStream.
  3. Enable failure_mode_allow to bypass while remediating.

Example fix

// before: server sends trailers, then more body
stream.Send(&ProcessingResponse{ResponseTrailers: ...})
stream.Send(&ProcessingResponse{ResponseBody: ...}) // -> error

// after: body fully drained before trailers
for body := range bodyCh { stream.Send(&ProcessingResponse{ResponseBody: body}) }
stream.Send(&ProcessingResponse{ResponseTrailers: ...})
Defensive patterns

Strategy: validation

Validate before calling

// Server: drain all body messages before sending response_trailers.
func sendInOrder(stream pb.ExternalProcessor_ProcessServer) error {
    // for body in bodies: stream.Send(responseBody(body))
    // then: stream.Send(responseTrailers(...))
    return nil
}

Try / catch

// Client: failure_mode_allow -> bypass.

Prevention

When it happens

Trigger: The server sends response_body after it has already sent response_trailers, or interleaves them out of order while response_trailer_mode is SEND.

Common situations: Server implementation that buffers and flushes trailers before residual body chunks. Concurrency bug in the server emitting trailer and body responses from separate goroutines.

Related errors


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