{"record":{"id":"da7c11da4d21dea9","repo":"grpc/grpc-go","slug":"external-processor-unexpectedly-sent-response-head","errorCode":null,"errorMessage":"external processor unexpectedly sent response headers when response header processing is disabled","messagePattern":"external processor unexpectedly sent response headers when response header processing is disabled","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/xds/httpfilter/extproc/ext_proc.go","lineNumber":1469,"sourceCode":"\t\t\t// response body message, fail the RPC.\n\t\t\tif cs.config.processingModes.responseTrailerMode == modeSend && cs.responseTrailerReady.HasFired() {\n\t\t\t\tcs.failProcStream(fmt.Errorf(\"external processor sent response body after response trailers were already processed\"))\n\t\t\t\treturn\n\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}","sourceCodeStart":1451,"sourceCodeEnd":1487,"githubUrl":"https://github.com/grpc/grpc-go/blob/0c51461d27177d997e14c642fe18c11668fc09a3/internal/xds/httpfilter/extproc/ext_proc.go#L1451-L1487","documentation":"Raised by recvFromProcServerLoop (ext_proc.go:1469) when responseHeaderMode is modeSkip but the ext_proc server sends a response_headers response. Mutating response headers the client was told not to send is a protocol violation; failProcStream fails the RPC unless failure_mode_allow bypasses it.","triggerScenarios":"Triggered when response_header_mode is SKIP and the server returns a ProcessingResponse with response_headers set (ext_proc.go:1467).","commonSituations":"Server assumes response header processing is always on, a templated handler that always returns response header mutations, or config drift between server expectations and the xDS processing_mode.","solutions":["On the server, only return response_headers when the client's ProtocolConfiguration indicates response header processing is enabled.","If response header mutation is desired, set response_header_mode to SEND in the xDS config.","Enable failure_mode_allow so the client tolerates the violation and proceeds to the dataplane.","Make the server handler mode-aware by reading the negotiated ProtocolConfiguration on stream start."],"exampleFix":"// before: server always sends response header mutation\nreturn &procpb.ProcessingResponse{Response: &procpb.ProcessingResponse_ResponseHeaders{...}}, nil\n\n// after: only when response header mode is enabled on the client\nif respHeaderModeEnabled {\n  return &procpb.ProcessingResponse{Response: &procpb.ProcessingResponse_ResponseHeaders{...}}, nil\n}","handlingStrategy":"fallback","validationCode":"// On the ext_proc SERVER: gate response_headers output on negotiated mode.\nfunc shouldEmitResponseHeaders(protocolCfg *procpb.ProtocolConfiguration) bool {\n    // The client advertises response header processing via the absence/presence\n    // negotiated out-of-band; mirror the xDS response_header_mode.\n    return responseHeaderModeEnabled // derived from your config + the client's protocol config\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(), \"unexpectedly sent response headers\") {\n    // server returned response_headers while client mode is SKIP\n}","preventionTips":["Server: only return response_headers when the negotiated response header mode is enabled.","Set response_header_mode to SEND in xDS if you actually want response header mutation.","Enable failure_mode_allow to tolerate the mismatch.","Make handlers mode-aware by reading ProtocolConfiguration at stream open."],"tags":["grpc","xds","extproc","envoy","protocol-violation","response-headers","processing-mode"],"backgroundTag":null,"analyzedSha":"0c51461d27177d997e14c642fe18c11668fc09a3","analyzedAt":"2026-08-11T14:49:15.055Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}