grpc/grpc-go · error

external processor sent response body before sending…

Error message

external processor sent response body before sending response headers

What it means

Raised by recvFromProcServerLoop (ext_proc.go:1446) when responseHeaderMode is SEND and the server sends a response_body response before the client has forwarded response headers to it (responseHeadersReady has not fired). Response body must follow response headers in the negotiated ordering; failProcStream fails the RPC unless failure_mode_allow bypasses it.

Solutions

  1. On the server, strictly order responses: only send response_body after you have received the response_headers request from the client.
  2. If you do not need response header processing, set response_header_mode to SKIP (then this ordering guard is not applied).
  3. Enable failure_mode_allow so the client tolerates the ordering violation and bypasses ext_proc.
  4. Add server-side logging of received ProcessingRequest types to confirm the request/response ordering.

Example fix

// before: server sends response body without waiting for response headers
func handle(stream) {
  stream.Send(&procpb.ProcessingResponse{Response: &procpb.ProcessingResponse_ResponseBody{...}})
}

// after: wait for the response headers request first
for {
  req, _ := stream.Recv()
  switch req.Request.(type) {
  case *procpb.ProcessingRequest_ResponseHeaders:
    stream.Send(&procpb.ProcessingResponse{Response: &procpb.ProcessingResponse_ResponseHeaders{...}})
  case *procpb.ProcessingRequest_ResponseBody:
    stream.Send(&procpb.ProcessingResponse{Response: &procpb.ProcessingResponse_ResponseBody{...}})
  }
}
Defensive patterns

Strategy: fallback

Validate before calling

// On the ext_proc SERVER: enforce strict request/response ordering per stream.
type phase int
const (
    phaseRespHeaders phase = iota
    phaseRespBody
    phaseRespTrailers
)
// Only allow sending response_body after phase >= phaseRespBody is reached
// (i.e. after a response_headers request was received and answered).

Try / catch

filter.failure_mode_allow = true
if st, ok := status.FromError(err); ok && st.Code() == codes.Internal &&
    strings.Contains(st.Message(), "response body before sending response headers") {
    // server sent response_body before the client forwarded response headers
}

Prevention

When it happens

Trigger: Triggered when response_header_mode == SEND and the server emits response_body (ext_proc.go:1436) before the client has sent the response_headers event the server must reply to first — e.g. the server sends a response body mutation unprompted on a fresh stream.

Common situations: Server handler that fires response body mutations on a timer or out of band without waiting for the response_headers request, or a server that conflates request and response phases and starts emitting response_body immediately.

Related errors


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

Appendix: source

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

			if !ok {
				return
			}
			if streamedResp.GetEndOfStream() {
				cs.discardRequests.Store(true)
			}
			cs.mutatedReqBuffer.Put(streamedResp)

		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
			}

View on GitHub (pinned to 0c51461d27)