{"record":{"id":"e333cd5c626f50a5","repo":"grpc/grpc-go","slug":"external-processor-unexpectedly-sent-response-trai","errorCode":null,"errorMessage":"external processor unexpectedly sent response trailers when response trailer processing is disabled","messagePattern":"external processor unexpectedly sent response trailers when response trailer processing is disabled","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/xds/httpfilter/extproc/ext_proc.go","lineNumber":1499,"sourceCode":"\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\"))\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()","sourceCodeStart":1481,"sourceCodeEnd":1517,"githubUrl":"https://github.com/grpc/grpc-go/blob/0c51461d27177d997e14c642fe18c11668fc09a3/internal/xds/httpfilter/extproc/ext_proc.go#L1481-L1517","documentation":"Raised by recvFromProcServerLoop (ext_proc.go:1499) when responseTrailerMode is modeSkip but the ext_proc server sends a response_trailers response. Mutating response trailers the client never agreed to forward is a protocol violation; failProcStream fails the RPC unless failure_mode_allow bypasses it.","triggerScenarios":"Triggered when response_trailer_mode is SKIP and the server returns a ProcessingResponse with response_trailers set (ext_proc.go:1497).","commonSituations":"Server assumes response trailer processing is on, a shared handler that always emits trailer mutations, or config drift between server expectations and the xDS processing_mode.","solutions":["On the server, only return response_trailers when the client's ProtocolConfiguration indicates response trailer processing is enabled.","If trailer mutation is desired, set response_trailer_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 read the negotiated modes and skip trailer output when disabled."],"exampleFix":"// before: server always sends response trailer mutation\nreturn &procpb.ProcessingResponse{Response: &procpb.ProcessingResponse_ResponseTrailers{...}}, nil\n\n// after: only when response trailer mode is enabled on the client\nif respTrailerModeEnabled {\n  return &procpb.ProcessingResponse{Response: &procpb.ProcessingResponse_ResponseTrailers{...}}, nil\n}","handlingStrategy":"fallback","validationCode":"// On the ext_proc SERVER: gate response_trailers output on negotiated mode.\nfunc shouldEmitResponseTrailers(responseTrailerModeEnabled bool) bool {\n    return responseTrailerModeEnabled\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 trailers\") {\n    // server returned response_trailers while client mode is SKIP\n}","preventionTips":["Server: only return response_trailers when the negotiated response trailer mode is enabled.","Set response_trailer_mode to SEND in xDS if you actually want trailer 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-trailers","processing-mode"],"backgroundTag":null,"analyzedSha":"0c51461d27177d997e14c642fe18c11668fc09a3","analyzedAt":"2026-08-11T14:49:15.055Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}