{"record":{"id":"ef6ae1870d026b90","repo":"grpc/grpc-go","slug":"external-processor-returned-an-unexpected-message","errorCode":null,"errorMessage":"external processor returned an unexpected message type %T, expected request headers response","messagePattern":"external processor returned an unexpected message type %T, expected request headers response","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/xds/httpfilter/extproc/ext_proc.go","lineNumber":1715,"sourceCode":"// returns false if the proc stream needs to be bypassed, or failed due to an\n// error, or aborted early by a processor-initiated immediate response.\nfunc (cs *clientStream) processInitialHeaders(ctx context.Context, newStream func(context.Context, ...grpc.CallOption) (grpc.ClientStream, error), opts []grpc.CallOption) bool {\n\tresp, err := cs.procStream.Recv()\n\tif err != nil {\n\t\tcs.handleHeaderError(err, newStream, opts)\n\t\treturn false\n\t}\n\tif resp.GetRequestDrain() {\n\t\tcs.triggerBypass()\n\t}\n\tif resp.GetImmediateResponse() != nil {\n\t\tcs.handleImmediateResponse(resp.GetImmediateResponse(), newStream, opts)\n\t\treturn false\n\t}\n\n\theader := resp.GetRequestHeaders()\n\tif header == nil {\n\t\terr := fmt.Errorf(\"external processor returned an unexpected message type %T, expected request headers response\", resp.GetResponse())\n\t\tcs.handleHeaderError(err, newStream, opts)\n\t\treturn false\n\t}\n\t// Check if status in header response is CONTINUE; if not, fail the stream.\n\tif status := header.GetResponse().GetStatus(); status != v3procservicepb.CommonResponse_CONTINUE {\n\t\tcs.handleHeaderError(fmt.Errorf(\"external processor returned unexpected status %v for request headers, expected %v\", status, v3procservicepb.CommonResponse_CONTINUE), newStream, opts)\n\t\treturn false\n\t}\n\t// Mutate the outgoing headers with additions and removals received from the\n\t// external processor.\n\toutgoingMD, _ := metadata.FromOutgoingContext(ctx)\n\tif outgoingMD == nil {\n\t\toutgoingMD = metadata.MD{}\n\t}\n\tif err = cs.applyMutations(header.GetResponse().GetHeaderMutation(), outgoingMD); err != nil {\n\t\tcs.handleHeaderError(err, newStream, opts)\n\t\treturn false\n\t}","sourceCodeStart":1697,"sourceCodeEnd":1733,"githubUrl":"https://github.com/grpc/grpc-go/blob/0c51461d27177d997e14c642fe18c11668fc09a3/internal/xds/httpfilter/extproc/ext_proc.go#L1697-L1733","documentation":"After the first Recv() on the extproc stream, processInitialHeaders (ext_proc.go:1714) expects a ProcessingResponse carrying a request_headers response, an immediate_response, or a request_drain. If none of those is set (GetRequestHeaders() == nil), the server returned an out-of-protocol message variant (e.g., response_headers, body_response, or response_trailers) as its first reply.","triggerScenarios":"External processor sends a ProcessingResponse whose oneof is set to response_headers, response_body, or response_trailers before ever sending a request_headers response as the first message of the stream.","commonSituations":"Misimplemented external processor that dispatches responses out of phase, or a server that mirrors request types instead of matching the request phase. Server built against an older or custom proto layout.","solutions":["Fix the external processor so the first response on the stream is a ProcessingResponse with the request_headers variant set (or an immediate_response / request_drain).","Verify the server branches on the incoming ProcessingRequest oneof and replies with the matching response variant.","Update the external processor proto/code to the same version grpc-go uses."],"exampleFix":"// before (server)\nreturn &procpb.ProcessingResponse{Response: &procpb.ProcessingResponse_ResponseHeaders{...}}\n\n// after\nreturn &procpb.ProcessingResponse{Response: &procpb.ProcessingResponse_RequestHeaders{RequestHeaders: &procpb.HeadersResponse{Response: &procpb.CommonResponse{Status: procpb.CommonResponse_CONTINUE}}}}","handlingStrategy":"validation","validationCode":"// Server-side: enforce that the first response is a request_headers response\nfunc firstResponse(req *procpb.ProcessingRequest) *procpb.ProcessingResponse {\n    switch req.Request.(type) {\n    case *procpb.ProcessingRequest_RequestHeaders:\n        return &procpb.ProcessingResponse{Response: &procpb.ProcessingResponse_RequestHeaders{RequestHeaders: &procpb.HeadersResponse{Response: &procpb.CommonResponse{Status: procpb.CommonResponse_CONTINUE}}}}\n    }\n    return nil // never lead with another variant\n}","typeGuard":null,"tryCatchPattern":"// Client-side: the stream is failed/bypassed; the RPC surfaces codes.Internal.\nif err := stream.SendMsg(...); err != nil {\n    if st, ok := status.FromError(err); ok && strings.Contains(st.Message(), \"unexpected message type\") {\n        // external processor is non-conformant; fix the server\n    }\n}","preventionTips":["Branch the server response on the incoming ProcessingRequest oneof and reply with the matching response variant.","Add a conformance test that asserts the first response on a fresh stream is request_headers.","Pin the external processor proto version to match grpc-go."],"tags":["extproc","grpc","protocol","xds","external-processor"],"backgroundTag":null,"analyzedSha":"0c51461d27177d997e14c642fe18c11668fc09a3","analyzedAt":"2026-08-11T14:49:15.055Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}