Métadonnées, Intercepteurs, Validation et Gestion d'Erreurs en gRPC avec Go

1. Le Mécanisme des Métadonnées (metadata) dans gRPC

gRPC permet d'appeler des fonctions distantes comme des fonctions locales. Chaque appel RPC peut transporter des informations utiles via le mécanisme de métadonnées. Les métadonnées sont stockées sous forme de paires clé-valeur, où la clé est une string et la valeur est un tableau de chaînes de caractères ([]string).

Les métadonnées permettent au client et au serveur de s'échanger des informations contextuelles sur un appel, à l'image des en-têtes de requête et de réponse HTTP. La durée de vie des métadonnées est celle de l'appel RPC.


// Création de métadonnées
// Le type MD est une map avec des clés string et des valeurs []string.
type MD map[string][]string

// Première méthode : utiliser New()
md := metadata.New(map[string]string{"cle1": "valeur1", "cle2": "valeur2"})

// Deuxième méthode : utiliser Pairs()
// Les clés sont insensibles à la casse et sont converties en minuscules.
md := metadata.Pairs(
    "Cle1", "valeur1",
    "Cle1", "valeur1_2", // "Cle1" aura pour valeur []string{"valeur1", "valeur1_2"}
    "Cle2", "valeur2",
)

Envoi de métadonnées (côté client)


md := metadata.Pairs("cle", "valeur")
// Créer un contexte avec les métadonnées
ctx := metadata.NewOutgoingContext(context.Background(), md)
// Appel RPC simple
reponse, err := client.MonRPC(ctx, maRequete)

Réception de métadonnées (côté serveur)


func (s *MonServeur) MonRPC(ctx context.Context, in *pb.MaRequete) (*pb.MaReponse, error) {
    md, ok := metadata.FromIncomingContext(ctx)
    if ok {
        // Traitement des métadonnées
    }
}

1.1 Fichier Proto


syntax = "proto3";
option go_package = ".;proto";

service Hello {
    rpc Hello(HelloRequest) returns (HelloResponse);
}

message HelloRequest {
    string name = 1;
}

message HelloResponse {
    string reply = 1;
}

1.2 Génération des fichiers Go


protoc --go_out=. ./hello.proto
protoc --go-grpc_out=. --go-grpc_opt=require_unimplemented_servers=false ./hello.proto

1.3 Code serveur


package main

import (
    "context"
    "fmt"
    "net"
    "go_test_learn/meta_proto/proto"
    "google.golang.org/grpc"
    "google.golang.org/grpc/metadata"
)

type HelloServer struct{}

func (s *HelloServer) Hello(ctx context.Context, req *proto.HelloRequest) (*proto.HelloResponse, error) {
    md, ok := metadata.FromIncomingContext(ctx)
    if ok {
        for key, values := range md {
            fmt.Println(key, values)
        }
        if pwd, exists := md["password"]; exists {
            fmt.Println(pwd[0])
        }
    } else {
        fmt.Println("Échec de récupération des métadonnées")
    }
    fmt.Println(req.Name)
    return &proto.HelloResponse{Reply: "Données reçues du client : " + req.Name}, nil
}

func main() {
    g := grpc.NewServer()
    proto.RegisterHelloServer(g, &HelloServer{})
    listener, err := net.Listen("tcp", "0.0.0.0:50052")
    if err != nil {
        panic("Échec du démarrage du serveur")
    }
    g.Serve(listener)
}

1.4 Code client


package main

import (
    "context"
    "fmt"
    "go_test_learn/meta_proto/proto"
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials/insecure"
    "google.golang.org/grpc/metadata"
)

func main() {
    conn, err := grpc.Dial("127.0.0.1:50052", grpc.WithTransportCredentials(insecure.NewCredentials()))
    if err != nil {
        panic("Échec de la connexion au serveur")
    }
    defer conn.Close()
    client := proto.NewHelloClient(conn)
    req := &proto.HelloRequest{Name: "alice"}

    // Création des métadonnées
    md := metadata.New(map[string]string{"name": "alice", "password": "456"})
    ctx := metadata.NewOutgoingContext(context.Background(), md)
    res, err := client.Hello(ctx, req)
    if err != nil {
        panic("Échec de l'appel RPC")
    }
    fmt.Println(res.Reply)
}

2. Les Intercepteurs (Interecptors) dans gRPC

Les intercepteurs sont un mécanisme puissant permettant d'exécuter une logique personnalisée avant ou après un appel RPC, à la manière des middlewares dans Gin. Ils sont utiles pour l'authentification, la journalisation, etc.

2.1 Intercepteur serveur (grpc.UnaryInterceptor)

Le type de la fonction interceptrice est : type UnaryServerInterceptor func(ctx context.Context, req interface{}, info *UnaryServerInfo, handler UnaryHandler) (resp interface{}, err error)


// Exemple d'intercepteur
intercepteur := func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
    fmt.Println("Nouvelle requête reçue")
    debut := time.Now()
    res, err := handler(ctx, req)
    fmt.Println("Requête terminée")
    fmt.Println("Durée :", time.Since(debut))
    return res, err
}
opt := grpc.UnaryInterceptor(intercepteur)
g := grpc.NewServer(opt)

2.2 Intercepteur client (UnaryClientInterceptor)

Le type de la fonction interceptrice est : type UnaryClientInterceptor func(ctx context.Context, method string, req, reply interface{}, cc *ClientConn, invoker UnaryInvoker, opts ...CallOption) error


intercepteur := func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
    debut := time.Now()
    fmt.Println("Intercepteur client")
    err := invoker(ctx, method, req, reply, cc, opts...)
    fmt.Printf("Durée : %s\n", time.Since(debut))
    return err
}
opt := grpc.WithUnaryInterceptor(intercepteur)
conn, err := grpc.Dial("127.0.0.1:50052", opt, grpc.WithTransportCredentials(insecure.NewCredentials()))

2.3 Intercepteurs Open Source

La bibliothèque go-grpc-middleware fournit des intercepteurs prêts à l'emploi.


// Exemple avec grpc_auth et grpc_recovery
maFonctionAuth := func(ctx context.Context) (context.Context, error) {
    fmt.Println("Authentification effectuée")
    return ctx, nil
}
opt := grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(
    grpc_auth.UnaryServerInterceptor(maFonctionAuth),
    grpc_recovery.UnaryServerInterceptor(),
))
g := grpc.NewServer(opt)

3. Authentification avec Métadonnées et Intercepteurs

3.1 Implémentation personnalisée

Côté serveur :```

package main

import ( "context" "fmt" "net" "go_test_learn/interpret_proto/proto" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" )

type HelloServer struct{}

func (s *HelloServer) Hello(ctx context.Context, req *proto.HelloRequest) (*proto.HelloResponse, error) { fmt.Println(req.Name) return &proto.HelloResponse{Reply: "Données reçues du client : " + req.Name}, nil }

func main() { intercepteur := func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) { fmt.Println("Vérification des identifiants") md, ok := metadata.FromIncomingContext(ctx) if !ok { return resp, status.Error(codes.Unauthenticated, "Aucune information d'authentification") } nom := "" motDePasse := "" if names, exists := md["name"]; exists { nom = names[0] } if pass, exists := md["password"]; exists { motDePasse = pass[0] } if nom == "admin" && motDePasse == "secret" { return handler(ctx, req) } return resp, status.Error(codes.Unauthenticated, "Identifiants incorrects") } g := grpc.NewServer(grpc.UnaryInterceptor(intercepteur)) proto.RegisterHelloServer(g, &HelloServer{}) listener, _ := net.Listen("tcp", "0.0.0.0:50052") g.Serve(listener) }


**Côté client :**```

package main

import (
    "context"
    "fmt"
    "time"
    "go_test_learn/interpret_proto/proto"
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials/insecure"
    "google.golang.org/grpc/metadata"
)

func main() {
    intercepteur := func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
        fmt.Println("Ajout des identifiants via l'intercepteur client")
        md := metadata.New(map[string]string{"name": "admin", "password": "secret"})
        ctx = metadata.NewOutgoingContext(context.Background(), md)
        return invoker(ctx, method, req, reply, cc, opts...)
    }
    conn, err := grpc.Dial("127.0.0.1:50052",
        grpc.WithUnaryInterceptor(intercepteur),
        grpc.WithTransportCredentials(insecure.NewCredentials()))
    if err != nil {
        panic("Échec de la connexion")
    }
    defer conn.Close()
    client := proto.NewHelloClient(conn)
    res, err := client.Hello(context.Background(), &proto.HelloRequest{Name: "test"})
    if err != nil {
        fmt.Println(err)
    } else {
        fmt.Println(res.Reply)
    }
}

3.2 Utilisation de WithPerRPCCredentials

Côté serveur : (identique à l'exemple précédent)```

// Le code serveur reste le même que dans 3.1


**Côté client (utilisant l'interface intégrée) :**```

package main

import (
    "context"
    "fmt"
    "go_test_learn/interpret_proto/proto"
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials/insecure"
)

// Implémentation de l'interface PerRPCCredentials
type MesIdentifiants struct{}

func (m MesIdentifiants) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
    return map[string]string{
        "name":     "admin",
        "password": "secret",
    }, nil
}

func (m MesIdentifiants) RequireTransportSecurity() bool {
    return false
}

func main() {
    opt := grpc.WithPerRPCCredentials(MesIdentifiants{})
    conn, err := grpc.Dial("127.0.0.1:50052",
        grpc.WithTransportCredentials(insecure.NewCredentials()),
        opt)
    if err != nil {
        panic("Échec de la connexion")
    }
    defer conn.Close()
    client := proto.NewHelloClient(conn)
    res, err := client.Hello(context.Background(), &proto.HelloRequest{Name: "alice"})
    if err != nil {
        fmt.Println(err)
    } else {
        fmt.Println(res.Reply)
    }
}

4. Validation des Champs avec protoc-gen-validate

La bibliothèque protoc-gen-validate permet de générer des validations à partir des annotations dans le fichier proto.

4.1 Fichier Proto


syntax = "proto3";
import "validate.proto";
option go_package = "./;proto";

service Hello {
    rpc Hello(Person) returns (Person);
}

message Person {
    uint64 id    = 1 [(validate.rules).uint64.gt    = 999];
    string email = 2 [(validate.rules).string.email = true];
    string mobile = 3 [(validate.rules).string = {
        pattern: "^1[3-9][0-9]{9}$",
    }];
}

4.2 Génération


protoc --go_out=. --validate_out="lang=go:." hello.proto
protoc --go-grpc_out=. --go-grpc_opt=require_unimplemented_servers=false --validate_out="lang=go:." hello.proto

4.3 Serveur avec validation


package main

import (
    "context"
    "fmt"
    "net"
    "awesomeProject/valdiate_proto/proto"
    "google.golang.org/grpc"
    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/status"
)

type HelloServer struct{}

func (s *HelloServer) Hello(ctx context.Context, req *proto.Person) (*proto.Person, error) {
    if err := req.Validate(); err != nil {
        return nil, err
    }
    return &proto.Person{Id: 1000, Email: "test@example.com", Mobile: "18953675221"}, nil
}

type Validator interface {
    Validate() error
}

func main() {
    intercepteur := func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
        if v, ok := req.(Validator); ok {
            if err := v.Validate(); err != nil {
                return resp, status.Error(codes.InvalidArgument, err.Error())
            }
        }
        return handler(ctx, req)
    }
    g := grpc.NewServer(grpc.UnaryInterceptor(intercepteur))
    proto.RegisterHelloServer(g, &HelloServer{})
    listener, _ := net.Listen("tcp", "0.0.0.0:50052")
    g.Serve(listener)
}

4.4 Client


package main

import (
    "context"
    "fmt"
    "awesomeProject/valdiate_proto/proto"
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials/insecure"
)

func main() {
    conn, err := grpc.Dial("127.0.0.1:50052", grpc.WithTransportCredentials(insecure.NewCredentials()))
    if err != nil {
        panic("Échec de la connexion")
    }
    defer conn.Close()
    client := proto.NewHelloClient(conn)
    req := &proto.Person{
        Id:     1000,
        Email:  "user@example.com",
        Mobile: "18956362551",
    }
    res, err := client.Hello(context.Background(), req)
    if err != nil {
        fmt.Println(err)
    } else {
        fmt.Println(res.Mobile)
    }
}

5. Codes d'État gRPC

Les codes d'état gRPC standards sont définis dans la documentation officielle : Status Codes.

6. Gestion des Erreurs en gRPC

6.1 Côté serveur


status.Error(codes.Unauthenticated, "Aucune information d'authentification")
status.New(codes.InvalidArgument, "Argument invalide").Err()
status.Newf(codes.NotFound, "Ressource %s introuvable", "mon_id").Err()

6.2 Côté client


s, ok := status.FromError(err)
if !ok {
    fmt.Println(s.Message())
    fmt.Println(s.Code())
}

7. Gestion des Délais (Timeouts) en gRPC

7.1 Côté client


ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
res, err := client.Hello(ctx, &request)
if err != nil {
    s, ok := status.FromError(err)
    if ok {
        fmt.Println(s.Message())
        fmt.Println(s.Code())
    }
}

7.2 Côté serveur (simulation d'un traitement long)


func (s *HelloServer) Hello(ctx context.Context, req *proto.HelloRequest) (*proto.HelloResponse, error) {
    fmt.Println(req.Name)
    fmt.Println("Attente de 5 secondes...")
    time.Sleep(5 * time.Second)
    return &proto.HelloResponse{Reply: "Données reçues : " + req.Name}, nil
}

Étiquettes: grpc Go metadata interceptor authentication

Publié le 9 juillet à 23h34