grpc/grpc-go · error

external processor unexpectedly sent duplicate response…

Error message

external processor unexpectedly sent duplicate response trailers after response trailers were already processed

What it means

Raised by recvFromProcServerLoop (ext_proc.go:1507) when the ext_proc server sends a second response_trailers response after trailers were already processed (responseTrailerReady already fired). Duplicate trailer mutations are not allowed; failProcStream fails the RPC unless failure_mode_allow bypasses it.

Solutions

  1. On the server, send response_trailers at most once per stream.
  2. Track per-stream state so the trailer path is not re-entered.
  3. Enable failure_mode_allow so a duplicate does not fail the user RPC.
  4. Add server-side unit tests asserting response_trailers is sent exactly once.

Example fix

// before: trailer mutation sent more than once
for { req, _ := stream.Recv(); stream.Send(respTrailers(req)) }

// after: send once, guarded by per-stream state
trailersSent := false
for {
  req, _ := stream.Recv()
  if _, ok := req.Request.(*procpb.ProcessingRequest_ResponseTrailers); ok && !trailersSent {
    stream.Send(respTrailers(req)); trailersSent = true
  }
}
Defensive patterns

Strategy: fallback

Validate before calling

// On the ext_proc SERVER: ensure response_trailers is sent at most once.
type streamState struct{ respTrailersSent bool }
func (s *streamState) allowRespTrailers() bool {
    if s.respTrailersSent { return false }
    s.respTrailersSent = true
    return true
}

Try / catch

filter.failure_mode_allow = true
if st, ok := status.FromError(err); ok && st.Code() == codes.Internal &&
    strings.Contains(st.Message(), "duplicate response trailers") {
    // server sent response_trailers more than once
}

Prevention

When it happens

Trigger: Triggered when the server sends response_trailers (ext_proc.go:1497) a second time on a stream where responseTrailerReady.HasFired() is already true.

Common situations: Server handler that re-enters its trailer code path, an unguarded loop sending trailer mutations more than once, or a buggy fan-out handler duplicating responses near stream end.

Related errors


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

Appendix: source

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

				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()
		}
	}
}

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

View on GitHub (pinned to 0c51461d27)