{"record":{"id":"9ad6cdf56c6a1681","repo":"grpc/grpc-go","slug":"external-processor-returned-unexpected-status-v-f","errorCode":null,"errorMessage":"external processor returned unexpected status %v for response headers, expected %v","messagePattern":"external processor returned unexpected status (.+?) for response headers, expected (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/xds/httpfilter/extproc/ext_proc.go","lineNumber":1485,"sourceCode":"\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()\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\"))","sourceCodeStart":1467,"sourceCodeEnd":1503,"githubUrl":"https://github.com/grpc/grpc-go/blob/0c51461d27177d997e14c642fe18c11668fc09a3/internal/xds/httpfilter/extproc/ext_proc.go#L1467-L1503","documentation":"Raised by recvFromProcServerLoop (ext_proc.go:1485) when the response_headers mutation the server sends has a status other than CONTINUE. For response headers the client only accepts CommonResponse.CONTINUE; any other status (e.g. the server trying to short-circuit) is a protocol violation. failProcStream fails the RPC unless failure_mode_allow bypasses it.","triggerScenarios":"Triggered when the server's response_headers response carries response.status != CONTINUE (ext_proc.go:1484).","commonSituations":"Server reuses a request-headers handler that returns RESET/REPLACE/ABSOLUTE for response headers, or attempts an immediate-response-style rejection via the header status on the response path (where only CONTINUE is valid).","solutions":["On the server, always set status = CommonResponse.CONTINUE on response_headers mutations.","Use the immediate_response field (not header status) if the server needs to short-circuit the RPC.","Enable failure_mode_allow so the client tolerates the bad status and bypasses ext_proc.","Separate the request-header and response-header handler code paths so status logic is not reused incorrectly."],"exampleFix":"// before: response header carries a non-CONTINUE status\nstream.Send(&procpb.ProcessingResponse{Response: &procpb.ProcessingResponse_ResponseHeaders{\n  ResponseHeaders: &procpb.HeadersResponse{Response: &procpb.CommonResponse{Status: procpb.CommonResponse_RESET}},\n}})\n\n// after: response headers must use CONTINUE\nResponseHeaders: &procpb.HeadersResponse{Response: &procpb.CommonResponse{Status: procpb.CommonResponse_CONTINUE}}","handlingStrategy":"fallback","validationCode":"// On the ext_proc SERVER: response headers must always carry CONTINUE.\nfunc buildResponseHeadersResponse() *procpb.HeadersResponse {\n    return &procpb.HeadersResponse{Response: &procpb.CommonResponse{Status: procpb.CommonResponse_CONTINUE}}\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(), \"unexpected status\") &&\n    strings.Contains(st.Message(), \"for response headers\") {\n    // server returned a non-CONTINUE status on response headers\n}","preventionTips":["Server: always set status = CONTINUE on response_headers responses.","Use immediate_response (not header status) to short-circuit the RPC.","Enable failure_mode_allow to tolerate the bad status.","Keep request-header and response-header status logic separate."],"tags":["grpc","xds","extproc","envoy","protocol-violation","response-headers","status"],"backgroundTag":null,"analyzedSha":"0c51461d27177d997e14c642fe18c11668fc09a3","analyzedAt":"2026-08-11T14:49:15.055Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}