{"record":{"id":"8b048cfede83b07c","repo":"grpc/grpc-go","slug":"external-processor-unexpectedly-sent-duplicate-res-8b048c","errorCode":null,"errorMessage":"external processor unexpectedly sent duplicate response trailers after response trailers were already processed","messagePattern":"external processor unexpectedly sent duplicate response trailers after response trailers were already processed","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/xds/httpfilter/extproc/ext_proc.go","lineNumber":1507,"sourceCode":"\t\t\t\tcs.failProcStream(err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\t// Signal that the response header is modified and ready to be sent to the\n\t\t\t// client, so that if there is any buffered response body, it can be sent\n\t\t\t// after the header.\n\t\t\tcs.fireResponseHeadersReady()\n\n\t\tcase resp.GetResponseTrailers() != nil:\n\t\t\tif cs.config.processingModes.responseTrailerMode == modeSkip {\n\t\t\t\tcs.failProcStream(fmt.Errorf(\"external processor unexpectedly sent response trailers when response trailer processing is disabled\"))\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !cs.trailerSent.Load() {\n\t\t\t\tcs.failProcStream(fmt.Errorf(\"external processor sent response trailers before response trailers were sent to it\"))\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif cs.responseTrailerReady.HasFired() {\n\t\t\t\tcs.failProcStream(fmt.Errorf(\"external processor unexpectedly sent duplicate response trailers after response trailers were already processed\"))\n\t\t\t\treturn\n\t\t\t}\n\t\t\ttrailer := resp.GetResponseTrailers()\n\t\t\tif err = cs.applyMutations(trailer.GetHeaderMutation(), cs.responseTrailers); err != nil {\n\t\t\t\tcs.failProcStream(err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\t// Signal that the response trailer is modified and ready to be sent to\n\t\t\t// the client.\n\t\t\tcs.fireResponseTrailerReady()\n\t\t}\n\t}\n}\n\nfunc (cs *clientStream) validateBodyResponse(bodyResp *v3procservicepb.BodyResponse) (*v3procservicepb.StreamedBodyResponse, bool) {\n\tif status := bodyResp.GetResponse().GetStatus(); status != v3procservicepb.CommonResponse_CONTINUE {\n\t\tcs.failProcStream(fmt.Errorf(\"external processor returned unexpected status %v for body response, expected %v\", status, v3procservicepb.CommonResponse_CONTINUE))\n\t\treturn nil, false","sourceCodeStart":1489,"sourceCodeEnd":1525,"githubUrl":"https://github.com/grpc/grpc-go/blob/0c51461d27177d997e14c642fe18c11668fc09a3/internal/xds/httpfilter/extproc/ext_proc.go#L1489-L1525","documentation":"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.","triggerScenarios":"Triggered when the server sends response_trailers (ext_proc.go:1497) a second time on a stream where responseTrailerReady.HasFired() is already true.","commonSituations":"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.","solutions":["On the server, send response_trailers at most once per stream.","Track per-stream state so the trailer path is not re-entered.","Enable failure_mode_allow so a duplicate does not fail the user RPC.","Add server-side unit tests asserting response_trailers is sent exactly once."],"exampleFix":"// before: trailer mutation sent more than once\nfor { req, _ := stream.Recv(); stream.Send(respTrailers(req)) }\n\n// after: send once, guarded by per-stream state\ntrailersSent := false\nfor {\n  req, _ := stream.Recv()\n  if _, ok := req.Request.(*procpb.ProcessingRequest_ResponseTrailers); ok && !trailersSent {\n    stream.Send(respTrailers(req)); trailersSent = true\n  }\n}","handlingStrategy":"fallback","validationCode":"// On the ext_proc SERVER: ensure response_trailers is sent at most once.\ntype streamState struct{ respTrailersSent bool }\nfunc (s *streamState) allowRespTrailers() bool {\n    if s.respTrailersSent { return false }\n    s.respTrailersSent = true\n    return true\n}","typeGuard":null,"tryCatchPattern":"filter.failure_mode_allow = true\nif st, ok := status.FromError(err); ok && st.Code() == codes.Internal &&\n    strings.Contains(st.Message(), \"duplicate response trailers\") {\n    // server sent response_trailers more than once\n}","preventionTips":["Server: send response_trailers exactly once per stream.","Guard the response-trailer code path with per-stream state.","Enable failure_mode_allow so duplicates degrade rather than fail.","Server unit test: assert response_trailers count == 1 across a full stream."],"tags":["grpc","xds","extproc","envoy","protocol-violation","duplicate","response-trailers"],"backgroundTag":null,"analyzedSha":"0c51461d27177d997e14c642fe18c11668fc09a3","analyzedAt":"2026-08-11T14:49:15.055Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}