{"record":{"id":"2a9fcb2613cb967b","repo":"grpc/grpc-go","slug":"external-processor-unexpectedly-set-end-of-stream","errorCode":null,"errorMessage":"external processor unexpectedly set end of stream in response body mutation","messagePattern":"external processor unexpectedly set end of stream in response body mutation","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/xds/httpfilter/extproc/ext_proc.go","lineNumber":1462,"sourceCode":"\t\t\t// RPC.\n\t\t\tif cs.config.processingModes.responseHeaderMode == modeSend && !cs.responseHeadersReady.HasFired() {\n\t\t\t\tcs.failProcStream(fmt.Errorf(\"external processor sent response body before sending response headers\"))\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// If mutated response trailers have been received before receiving the\n\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","sourceCodeStart":1444,"sourceCodeEnd":1480,"githubUrl":"https://github.com/grpc/grpc-go/blob/0c51461d27177d997e14c642fe18c11668fc09a3/internal/xds/httpfilter/extproc/ext_proc.go#L1444-L1480","documentation":"Raised by recvFromProcServerLoop (ext_proc.go:1462) when a response body mutation from the server carries EndOfStream set in its streamed body response. The client controls stream end, so the server setting EOS inside a response body mutation is rejected; failProcStream fails the RPC unless failure_mode_allow bypasses it.","triggerScenarios":"Triggered when validateBodyResponse returns a streamedResp whose EndOfStream is true (ext_proc.go:1461) inside a response_body message from the server.","commonSituations":"Server copies EndOfStream from the request body it received, a handler that signals stream end via the body mutation rather than by simply stopping, or misuse of the BodyResponse.body_mutation.streamed_response.end_of_stream field.","solutions":["On the server, never set body_mutation.streamed_response.end_of_stream (leave it false); EOS is the client's responsibility.","If the server wants to end the gRPC stream, half-close its own send side instead of mutating EOS.","Enable failure_mode_allow so the client bypasses ext_proc rather than failing the RPC.","Audit the server's BodyResponse construction to ensure end_of_stream is never populated."],"exampleFix":"// before: server sets end of stream inside the body mutation\nstream.Send(&procpb.ProcessingResponse{Response: &procpb.ProcessingResponse_ResponseBody{\n  ResponseBody: &procpb.BodyResponse{\n    Response: &procpb.CommonResponse{\n      BodyMutation: &procpb.BodyMutation{BodyMutation: &procpb.BodyMutation_StreamedResponse{\n        StreamedResponse: &procpb.StreamedBodyResponse{Body: chunk, EndOfStream: true},\n      }},\n    },\n  },\n}})\n\n// after: do not set EndOfStream in the body mutation\nStreamedResponse: &procpb.StreamedBodyResponse{Body: chunk}","handlingStrategy":"fallback","validationCode":"// On the ext_proc SERVER: never populate end_of_stream on the streamed body.\nfunc buildStreamedBody(chunk []byte) *procpb.StreamedBodyResponse {\n    return &procpb.StreamedBodyResponse{Body: chunk} // EndOfStream left false\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(), \"end of stream in response body mutation\") {\n    // server incorrectly set end_of_stream inside the body mutation\n}","preventionTips":["Server: leave BodyMutation.StreamedResponse.end_of_stream unset (false).","To end the stream, half-close the server's send side instead.","Enable failure_mode_allow so the RPC survives the violation.","Audit BodyResponse construction for any accidental EndOfStream copy from input."],"tags":["grpc","xds","extproc","envoy","protocol-violation","response-body","end-of-stream"],"backgroundTag":null,"analyzedSha":"0c51461d27177d997e14c642fe18c11668fc09a3","analyzedAt":"2026-08-11T14:49:15.055Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}