{"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":1341,"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":1323,"sourceCodeEnd":1359,"githubUrl":"https://github.com/grpc/grpc-go/blob/03255a9237b6eb32710f6bc4f2de9a675b99fe36/internal/xds/httpfilter/extproc/ext_proc.go#L1323-L1359","documentation":"The grpc-go external-processor (extproc) HTTP filter enforces a strict request/response ordering with the sidecar server. Response trailers may be sent by the server at most once per RPC; a one-shot event (responseTrailerReady) records that they have already been received and applied. If a second ProcessingResponse message carrying response_trailers arrives after that event has fired, the client treats it as a protocol violation and fails the extproc stream.","triggerScenarios":"The extproc server sends two distinct ProcessingResponse messages both with ResponseTrailers set during a single RPC, after the client has already processed the first one and fired responseTrailerReady.","commonSituations":"A buggy or stateful extproc proxy that buffers then re-flushes trailer mutations; a custom sidecar that unconditionally echoes trailers on every recv loop; concurrency in the server where two goroutines write trailers to the same stream.","solutions":["Audit the external processor server logic and ensure it emits a response_trailers ProcessingResponse at most once per RPC (track a per-stream 'trailersSent' flag).","Verify the server only sends trailer messages after the client forwarded response trailers to it, never proactively or on unrelated messages.","If using an Envoy-built or third-party sidecar, upgrade it; duplicate trailer messages are typically a server bug fixed in newer versions.","Enable failure_mode_allow in the extproc filter config so a misbehaving server bypasses instead of failing RPCs while you fix the server."],"exampleFix":"// before: extproc server re-sends trailers\nstream.Send(&procservicepb.ProcessingResponse{Response: &procservicepb.ProcessingResponse_ResponseTrailers{ResponseTrailers: tv}})\n// ...later, same stream...\nstream.Send(&procservicepb.ProcessingResponse{Response: &procservicepb.ProcessingResponse_ResponseTrailers{ResponseTrailers: tv}})\n\n// after: send response trailers at most once per RPC\nif !trailersSent {\n    stream.Send(&procservicepb.ProcessingResponse{Response: &procservicepb.ProcessingResponse_ResponseTrailers{ResponseTrailers: tv}})\n    trailersSent = true\n}","handlingStrategy":"fallback","validationCode":"// Client-side guard is limited; the stream-level duplicate is a server behavior.\n// Tolerate it via the extproc failure mode:\nif pm.FailureModeAllow { /* proc errors bypass instead of failing RPCs */ }","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Treat the extproc server as stateful: track per-stream flags so each message type is emitted at most once.","Enable failure_mode_allow so a server regression degrades gracefully while you investigate.","Add server-side integration tests that assert response-trailers is sent exactly once per RPC."],"tags":["extproc","grpc","xds","protocol","go"],"analyzedSha":"03255a9237b6eb32710f6bc4f2de9a675b99fe36","analyzedAt":"2026-08-07T00:29:34.215Z","schemaVersion":2},"datasetVersion":"2026-08-07T03:17:09.362Z"}