{"record":{"id":"4273cb6fc2c8cc26","repo":"grpc/grpc-go","slug":"external-processor-sent-response-headers-before-re","errorCode":null,"errorMessage":"external processor sent response headers before response headers were sent to it","messagePattern":"external processor sent response headers before response headers were sent to it","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/xds/httpfilter/extproc/ext_proc.go","lineNumber":1473,"sourceCode":"\t\t\t}\n\n\t\t\tstreamedResp, ok := cs.validateBodyResponse(resp.GetResponseBody())\n\t\t\tif !ok {\n\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}","sourceCodeStart":1455,"sourceCodeEnd":1491,"githubUrl":"https://github.com/grpc/grpc-go/blob/0c51461d27177d997e14c642fe18c11668fc09a3/internal/xds/httpfilter/extproc/ext_proc.go#L1455-L1491","documentation":"Raised by recvFromProcServerLoop (ext_proc.go:1473) when the ext_proc server sends a response_headers response before the client has sent it the response headers (responseHeaderSent is false). The server cannot mutate headers it has not received; failProcStream fails the RPC unless failure_mode_allow bypasses it.","triggerScenarios":"Triggered when the server emits a response_headers mutation (ext_proc.go:1467) on a stream where the client has not yet forwarded the response_headers event — e.g. the server proactively pushes a header mutation on stream open.","commonSituations":"Server that injects a fixed response header set at stream start rather than in response to the client's response_headers request, or a handler that confuses request_headers and response_headers phases.","solutions":["On the server, only send response_headers mutations in reply to a response_headers ProcessingRequest.","Set failure_mode_allow so the dataplane RPC is not failed by the premature mutation.","Add server-side request-type logging to confirm you only emit response_headers after receiving one.","If you only need to add a static header, do it inside the response_headers handler, not eagerly."],"exampleFix":"// before: server pushes a response header mutation immediately\nfunc handle(stream) {\n  stream.Send(&procpb.ProcessingResponse{Response: &procpb.ProcessingResponse_ResponseHeaders{...}})\n}\n\n// after: respond only to the matching request type\nfor {\n  req, _ := stream.Recv()\n  if _, ok := req.Request.(*procpb.ProcessingRequest_ResponseHeaders); ok {\n    stream.Send(&procpb.ProcessingResponse{Response: &procpb.ProcessingResponse_ResponseHeaders{...}})\n  }\n}","handlingStrategy":"fallback","validationCode":"// On the ext_proc SERVER: only emit response_headers in reply to a\n// response_headers request, never eagerly.\nfunc shouldAnswerResponseHeaders(req *procpb.ProcessingRequest, sent bool) bool {\n    _, isRespHdrs := req.GetRequest().(*procpb.ProcessingRequest_ResponseHeaders)\n    return isRespHdrs && sent\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(), \"response headers before response headers were sent to it\") {\n    // server pushed response_headers before the client forwarded them\n}","preventionTips":["Server: send response_headers only in reply to a response_headers ProcessingRequest.","Add static header mutations inside the response_headers handler, not at stream open.","Enable failure_mode_allow so a premature mutation does not fail the RPC.","Log received ProcessingRequest types server-side to confirm ordering."],"tags":["grpc","xds","extproc","envoy","protocol-violation","ordering","response-headers"],"backgroundTag":null,"analyzedSha":"0c51461d27177d997e14c642fe18c11668fc09a3","analyzedAt":"2026-08-11T14:49:15.055Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}