{"record":{"id":"587324ff759bea72","repo":"grpc/grpc-go","slug":"external-processor-unexpectedly-sent-duplicate-res","errorCode":null,"errorMessage":"external processor unexpectedly sent duplicate response headers after response headers were already processed","messagePattern":"external processor unexpectedly sent duplicate response headers after response headers were already processed","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/xds/httpfilter/extproc/ext_proc.go","lineNumber":1477,"sourceCode":"\t\t\t\treturn\n\t\t\t}\n\t\t\tif streamedResp.GetEndOfStream() {\n\t\t\t\tcs.failProcStream(fmt.Errorf(\"external processor unexpectedly set end of stream in response body mutation\"))\n\t\t\t\treturn\n\t\t\t}\n\t\t\tcs.mutatedRespBuffer.Put(streamedResp)\n\n\t\tcase resp.GetResponseHeaders() != nil:\n\t\t\tif cs.config.processingModes.responseHeaderMode == modeSkip {\n\t\t\t\tcs.failProcStream(fmt.Errorf(\"external processor unexpectedly sent response headers when response header processing is disabled\"))\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !cs.responseHeaderSent.Load() {\n\t\t\t\tcs.failProcStream(fmt.Errorf(\"external processor sent response headers before response headers were sent to it\"))\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif cs.responseHeadersReady.HasFired() {\n\t\t\t\tcs.failProcStream(fmt.Errorf(\"external processor unexpectedly sent duplicate response headers after response headers were already processed\"))\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\theader := resp.GetResponseHeaders()\n\t\t\t// Check if the status in the header response is CONTINUE; if not, fail\n\t\t\t// the stream.\n\t\t\tif status := header.GetResponse().GetStatus(); status != v3procservicepb.CommonResponse_CONTINUE {\n\t\t\t\tcs.failProcStream(fmt.Errorf(\"external processor returned unexpected status %v for response headers, expected %v\", status, v3procservicepb.CommonResponse_CONTINUE))\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif err = cs.applyMutations(header.GetResponse().GetHeaderMutation(), cs.responseHeader); 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 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()","sourceCodeStart":1459,"sourceCodeEnd":1495,"githubUrl":"https://github.com/grpc/grpc-go/blob/0c51461d27177d997e14c642fe18c11668fc09a3/internal/xds/httpfilter/extproc/ext_proc.go#L1459-L1495","documentation":"Raised by recvFromProcServerLoop (ext_proc.go:1477) when the ext_proc server sends a second response_headers response after response headers were already processed (responseHeadersReady already fired). Duplicate header mutations are not allowed; failProcStream fails the RPC unless failure_mode_allow bypasses it.","triggerScenarios":"Triggered when the server sends response_headers (ext_proc.go:1467) a second time on a stream where responseHeadersReady.HasFired() is already true.","commonSituations":"Server handler that re-enters its response-header code path (e.g. on each received message), an unguarded loop sending header mutations more than once, or a buggy fan-out handler duplicating responses.","solutions":["On the server, send response_headers at most once per stream.","Track per-stream state in the handler so the header 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_headers is sent exactly once."],"exampleFix":"// before: header mutation sent on every received message\nfor { req, _ := stream.Recv(); stream.Send(respHeaders(req)) }\n\n// after: send once, then only other phases\nheadersSent := false\nfor {\n  req, _ := stream.Recv()\n  if _, ok := req.Request.(*procpb.ProcessingRequest_ResponseHeaders); ok && !headersSent {\n    stream.Send(respHeaders(req)); headersSent = true\n  }\n}","handlingStrategy":"fallback","validationCode":"// On the ext_proc SERVER: ensure response_headers is sent at most once.\ntype streamState struct{ respHeadersSent bool }\nfunc (s *streamState) allowRespHeaders() bool {\n    if s.respHeadersSent { return false }\n    s.respHeadersSent = 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 headers\") {\n    // server sent response_headers more than once\n}","preventionTips":["Server: send response_headers exactly once per stream.","Guard the response-header code path with per-stream state.","Enable failure_mode_allow so duplicates degrade rather than fail.","Server unit test: assert response_headers count == 1 across a full stream."],"tags":["grpc","xds","extproc","envoy","protocol-violation","duplicate","response-headers"],"backgroundTag":null,"analyzedSha":"0c51461d27177d997e14c642fe18c11668fc09a3","analyzedAt":"2026-08-11T14:49:15.055Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}