grpc/grpc-go · error

external processor sent response trailers before response…

Error message

external processor sent response trailers before response trailers were sent to it

What it means

Raised by recvFromProcServerLoop (ext_proc.go:1503) when the ext_proc server sends a response_trailers response before the client has forwarded the response trailers (trailerSent is false). The server cannot mutate trailers it has not received; failProcStream fails the RPC unless failure_mode_allow bypasses it.

Solutions

  1. On the server, only send response_trailers mutations in reply to a response_trailers ProcessingRequest.
  2. Set failure_mode_allow so the premature mutation does not fail the RPC.
  3. Add server-side request-type logging to confirm response_trailers is only emitted after one is received.
  4. If injecting static trailers, do it inside the response_trailers handler, not eagerly.

Example fix

// before: server pushes trailer mutation immediately
func handle(stream) {
  stream.Send(&procpb.ProcessingResponse{Response: &procpb.ProcessingResponse_ResponseTrailers{...}})
}

// after: respond only to the matching request type
for {
  req, _ := stream.Recv()
  if _, ok := req.Request.(*procpb.ProcessingRequest_ResponseTrailers); ok {
    stream.Send(&procpb.ProcessingResponse{Response: &procpb.ProcessingResponse_ResponseTrailers{...}})
  }
}
Defensive patterns

Strategy: fallback

Validate before calling

// On the ext_proc SERVER: only emit response_trailers in reply to a
// response_trailers request.
func shouldAnswerResponseTrailers(req *procpb.ProcessingRequest) bool {
    _, ok := req.GetRequest().(*procpb.ProcessingRequest_ResponseTrailers)
    return ok
}

Try / catch

filter.failure_mode_allow = true
if st, ok := status.FromError(err); ok && st.Code() == codes.Internal &&
    strings.Contains(st.Message(), "response trailers before response trailers were sent to it") {
    // server pushed response_trailers before the client forwarded them
}

Prevention

When it happens

Trigger: Triggered when the server emits a response_trailers mutation (ext_proc.go:1497) on a stream where the client has not yet sent the response_trailers event — e.g. the server proactively pushes trailer mutations at stream open.

Common situations: Server that pushes a fixed set of trailers immediately instead of responding to the response_trailers request, or a handler that conflates headers and trailers phases.

Related errors


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

Appendix: source

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

				cs.failProcStream(fmt.Errorf("external processor returned unexpected status %v for response headers, expected %v", status, v3procservicepb.CommonResponse_CONTINUE))
				return
			}
			if err = cs.applyMutations(header.GetResponse().GetHeaderMutation(), cs.responseHeader); err != nil {
				cs.failProcStream(err)
				return
			}
			// Signal that the response header is modified and ready to be sent to the
			// client, so that if there is any buffered response body, it can be sent
			// after the header.
			cs.fireResponseHeadersReady()

		case resp.GetResponseTrailers() != nil:
			if cs.config.processingModes.responseTrailerMode == modeSkip {
				cs.failProcStream(fmt.Errorf("external processor unexpectedly sent response trailers when response trailer processing is disabled"))
				return
			}
			if !cs.trailerSent.Load() {
				cs.failProcStream(fmt.Errorf("external processor sent response trailers before response trailers were sent to it"))
				return
			}
			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()
		}
	}
}

View on GitHub (pinned to 0c51461d27)