Ai

Maîtriser le handling d’erreurs gRPC : codes de statut, métadonnées riches et patterns de production

Damien LarqueyDamien Larquey
February 24, 2026
8 min read
Maîtriser le handling d’erreurs gRPC : codes de statut, métadonnées riches et patterns de production

1. Objectif

Ce guide explique comment mettre en place un handling d’erreurs gRPC réellement « production‑ready » : choix précis des codes de statut, usage du modèle d’erreur riche de Google (métadonnées typées), et patterns de résilience (retries, hedging, circuit‑breakers, observabilité). L’objectif est d’éviter les classiques : erreurs UNKNOWN partout, messages impossibles à exploiter côté client, streams qui fuient et comportements différents selon les langages.

Le guide est volontairement langage‑agnostique, avec des exemples en Go, Java et .NET, transposables à n’importe quel runtime gRPC récent.

2. Prérequis

  • Runtimes gRPC récents (idéalement 1.65.0+ pour bénéficier du modèle d’erreur riche et des stratégies de retry/hedging intégrées) :
    • Go : google.golang.org/grpc v1.65.0+
    • Java : io.grpc:grpc-netty-shaded:1.65.x+
    • .NET : Grpc.AspNetCore 2.65.0+
  • Protocol Buffers : protoc 27+ et dépôt googleapis à jour pour disposer de google/rpc/status.proto et google/rpc/error_details.proto.
  • Environnement de dev prêt (Go 1.23+, Java 21+, .NET 8/10, etc.).
  • Outils de debug :
    • grpcurl pour invoquer vos RPC depuis la CLI,
    • grpc-health-probe pour les checks Kubernetes.
  • Stack d’observabilité de base :
    • OpenTelemetry (traces + logs structurés),
    • Prometheus ou équivalent pour les métriques.

Point de vigilance : synchroniser les définitions proto Google (googleapis) sur toutes les équipes. Des versions différentes entraînent facilement des erreurs UNIMPLEMENTED ou des détails d’erreur non parsables.

3. Mise en œuvre étape par étape

Étape 1 – Mettre à niveau la stack gRPC et les protos

Avant de toucher au code des erreurs, stabiliser la base : même version majeure de gRPC et des protos Google pour tous les services et clients.

// Exemple Go (go.mod)
module example.com/usersvc

go 1.23

require (
    google.golang.org/grpc v1.65.0
    google.golang.org/protobuf v1.35.0
    // googleapis est consommé via vos fichiers .proto
)

Générez ensuite les protos d’erreur Google pour votre langage (exemple Go) :

protoc \
  -I . \
  -I ./third_party/googleapis \
  --go_out=. \
  --go-grpc_out=. \
  google/rpc/status.proto \
  google/rpc/error_details.proto

Le même principe s’applique en Java, .NET ou Rust : assurez‑vous que google.rpc.Status et les messages de error_details.proto sont générés et inclus dans vos builds.

Maîtriser le handling d’erreurs gRPC : codes de statut, métadonnées riches et patterns de production

Étape 2 – Cartographier correctement les 23 codes gRPC

gRPC définit 23 codes standard. En pratique, concentrer l’usage sur un sous‑ensemble bien défini améliore énormément le comportement des clients.

  • OK : succès normal.
  • INVALID_ARGUMENT : validation côté serveur, entrée invalide (champ manquant, format incorrect).
  • FAILED_PRECONDITION : préconditions métier non satisfaites (ex : modèle ML non chargé, état incohérent).
  • NOT_FOUND : ressource absente.
  • ALREADY_EXISTS : tentative de création d’un doublon.
  • PERMISSION_DENIED / UNAUTHENTICATED : problèmes d’authz/authn.
  • RESOURCE_EXHAUSTED : quotas ou limites de capacité atteints.
  • UNAVAILABLE / DEADLINE_EXCEEDED : erreurs transitoires, typiquement éligibles aux retries.
  • INTERNAL : bug serveur. À réserver aux cas réellement inattendus.

Règle d’or : INVALID_ARGUMENT, NOT_FOUND et FAILED_PRECONDITION signifient « corrige la requête ». UNAVAILABLE et DEADLINE_EXCEEDED signifient « tu peux retenter plus tard ». INTERNAL = bug serveur, à monitorer comme tel.

Exemple Go (RPC unary) :

import (
    "context"
    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/status"
)

func (s *Server) GetUser(
    ctx context.Context,
    req *pb.GetUserRequest,
) (*pb.User, error) {
    if req.GetUserId() == "" {
        return nil, status.Error(codes.InvalidArgument, "user_id est requis")
    }

    user, err := s.repo.FindByID(ctx, req.GetUserId())
    if err == repo.ErrNotFound {
        return nil, status.Error(codes.NotFound, "utilisateur introuvable")
    }
    if err != nil {
        // ici probablement INTERNAL
        return nil, status.Error(codes.Internal, "erreur interne")
    }

    return user, nil
}

En Java, l’équivalent serait :

if (request.getUserId().isEmpty()) {
    throw Status.INVALID_ARGUMENT
        .withDescription("user_id est requis")
        .asRuntimeException();
}

Étape 3 – Exploiter le modèle d’erreur riche (métadonnées typées)

Un simple message texte ne suffit pas pour piloter le comportement client, surtout dans un SI polyglotte. Le modèle d’erreur riche de Google repose sur google.rpc.Status et des détails typés encapsulés via Any (ex : ErrorInfo, BadRequest, etc.). Ces détails sont envoyés dans les trailers gRPC.

Exemple Java : renvoyer un INVALID_ARGUMENT avec un détail ErrorInfo décrivant précisément la raison métier.

import com.google.protobuf.Any;
import com.google.rpc.ErrorInfo;
import com.google.rpc.Status;
import io.grpc.protobuf.StatusProto;

ErrorInfo errorInfo = ErrorInfo.newBuilder()
    .setReason("UNSUPPORTED_COMMODITY")
    .setDomain("com.example.trading")
    .putMetadata("expected", "Commodity1,Commodity2")
    .putMetadata("actual", "Commodity5")
    .build();

Status status = Status.newBuilder()
    .setCode(com.google.rpc.Code.INVALID_ARGUMENT.getNumber())
    .setMessage("Matière première non supportée")
    .addDetails(Any.pack(errorInfo))
    .build();

responseObserver.onError(StatusProto.toStatusRuntimeException(status));

Côté Go, extraction des détails :

import (
    "google.golang.org/grpc/status"
    "google.golang.org/genproto/googleapis/rpc/errdetails"
)

st, ok := status.FromError(err)
if ok {
    for _, d := range st.Details() {
        switch info := d.(type) {
        case *errdetails.ErrorInfo:
            logger.Warn("requête invalide",
                "reason", info.GetReason(),
                "expected", info.GetMetadata()["expected"],
                "actual", info.GetMetadata()["actual"],
            )
        }
    }
}

Limite importante : headers + trailers doivent rester sous ~8 Ko au total. Éviter d’y mettre des payloads volumineux ; privilégier des codes de raison, des IDs et du contexte minimal.

Étape 4 – Gérer correctement les erreurs en streaming

Les streams (server‑streaming, client‑streaming, bidi) compliquent la donne : vous ne voulez pas casser systématiquement la connexion dès qu’une erreur métier mineure survient.

Pattern recommandé :

  • Pour les erreurs précoces (validation de la requête avant tout envoi) : retour classique d’une erreur gRPC avec Status riche.
  • Pour les erreurs tardives de logique métier dans un stream long vivant :
    • soit retour d’une erreur gRPC en fin de stream,
    • soit envoi d’un dernier message de type « FinalStatus » contenant un google.rpc.Status, tout en laissant le stream se terminer proprement.

Exemple Go (server‑streaming, erreur de validation précoce) :

func (s *Server) StreamQuotes(
    req *pb.StreamQuotesRequest,
    stream pb.QuotesService_StreamQuotesServer,
) error {
    if !s.isSupported(req.Symbol) {
        st := status.New(codes.InvalidArgument, "symbole non supporté")
        br := &errdetails.BadRequest{
            FieldViolations: []*errdetails.BadRequest_FieldViolation{
                {
                    Field:       "symbol",
                    Description: "Seuls BTC, ETH sont supportés",
                },
            },
        }
        st, _ = st.WithDetails(br)
        return st.Err()
    }

    // ... envoi des messages
}

Astuce : toujours gérer context cancellation côté serveur pour éviter les goroutines zombies :

for {
    select {
    case <-stream.Context().Done():
        return stream.Context().Err() // CANCELLED / DEADLINE_EXCEEDED
    case q := <-quotes:
        if err := stream.Send(q); err != nil {
            return err
        }
    }
}

Étape 5 – Configurer retries, hedging et circuit‑breakers

Une fois les codes bien mappés, les clients peuvent appliquer des politiques de retry intelligentes : ne retenter que sur UNAVAILABLE, RESOURCE_EXHAUSTED, DEADLINE_EXCEEDED, jamais sur INVALID_ARGUMENT ou PERMISSION_DENIED.

Exemple de service_config JSON côté client (gRPC core) :

{
  "methodConfig": [{
    "name": [{"service": "example.UserService"}],
    "retryPolicy": {
      "maxAttempts": 4,
      "initialBackoff": "0.2s",
      "maxBackoff": "2s",
      "backoffMultiplier": 1.6,
      "retryableStatusCodes": [
        "UNAVAILABLE",
        "RESOURCE_EXHAUSTED"
      ]
    },
    "hedgingPolicy": {
      "maxAttempts": 3,
      "hedgingDelay": "50ms",
      "nonFatalStatusCodes": [
        "DEADLINE_EXCEEDED",
        "UNAVAILABLE"
      ]
    }
  }]
}

Le hedging (envoi de requêtes en parallèle avec annulation de celles qui arrivent trop tard) réduit souvent la latence p99 d’environ 40 % dans les benchmarks internes sur les réseaux instables, au prix d’un peu plus de charge.

Côté mesh (Istio/Envoy), compléter avec un circuit‑breaker (max connexions, erreur rate‑based) pour couper rapidement en cas de tempête d’erreurs UNAVAILABLE ou RESOURCE_EXHAUSTED.

4. Vérification

  • Tests unitaires :
    • vérifier que chaque scénario de validation renvoie le bon code (INVALID_ARGUMENT, NOT_FOUND, etc.),
    • vérifier la présence et le contenu des détails typés (ErrorInfo, BadRequest).
  • Tests d’intégration avec grpcurl :
    • envoyer une requête invalide : grpcurl -d '{"user_id":""}' localhost:8080 user.v1.UserService/GetUser,
    • contrôler le code et le message retournés, ainsi que les trailers si vous les logguez côté serveur.
  • Observabilité :
    • exposer des métriques type grpc_server_errors_total{code="INVALID_ARGUMENT"},
    • vérifier dans les traces OpenTelemetry que le code gRPC et un identifiant d’erreur (reason) sont attachés aux spans.

Une implémentation mature permet de savoir en un coup d’œil : quel est le top N des codes d’erreur par service, combien sont dues à des erreurs client vs serveur, et quel est l’impact sur la latence (p95/p99 avant/après hedging).

5. Troubleshooting

  • Trop de UNKNOWN ou INTERNAL :
    • installer un interceptor/middleware global qui convertit toutes les panics en INTERNAL avec un ErrorInfo de type « BUG » (stacktrace uniquement en environnement non‑prod),
    • remapper proprement les erreurs métier connues vers INVALID_ARGUMENT, FAILED_PRECONDITION, etc.
  • Clients qui ne voient pas les détails d’erreur :
    • vérifier que vous utilisez bien les helpers liés au modèle riche (ex : StatusProto.toStatusRuntimeException en Java),
    • vérifier la génération des protos google.rpc.* côté client.
  • Erreurs UNIMPLEMENTED inattendues :
    • souvent dues à des désynchronisations proto (mauvais package ou service, vieux stubs clients),
    • vérifier que les binaires serveur et client sont compilés à partir des mêmes versions de .proto.
  • Streams qui ne se ferment jamais :
    • oublis de gestion de ctx.Done() côté serveur,
    • clients qui ne consomment pas entièrement le stream ; ajouter des timeouts et vérifier les traces.
  • Trailers trop gros (> 8 Ko) :
    • ne jamais sérialiser un gros objet métier dans les détails d’erreur,
    • envoyer uniquement un identifiant et récupérer le détail complet depuis un store dédié (logging, data store d’erreurs).

6. Prochaines étapes et considérations opérationnelles

  • Standardiser un petit vocabulaire d’erreurs pour votre organisation :
    • un ensemble de ErrorInfo.reason documentés,
    • une convention pour les champs metadata (ex : tenant_id, request_id, quota_name).
  • Intégrer au CI/CD :
    • tests de contrat gRPC (client ↔ serveur) avec validation des codes de statut attendus,
    • linter interne interdisant l’usage direct de codes.Unknown / Status.UNKNOWN en production.
  • Observabilité avancée :
    • dashboards dédiés aux codes gRPC et aux raisons d’erreur,
    • corrélation avec les taux de retry, le hedging et l’ouverture des circuit‑breakers.
  • Sécurité :
    • sanitiser les messages exposés au client (pas de stacktrace ni d’info sensible en prod),
    • garder les détails complets uniquement dans les logs internes.
  • Interop HTTP/REST (grpc‑gateway, Envoy) :
    • mapper explicitement les codes gRPC vers les bons codes HTTP (ex : INVALID_ARGUMENT400, NOT_FOUND404, PERMISSION_DENIED403),
    • s’assurer que les détails d’erreur restent cohérents entre clients REST et gRPC.

Avec ces patterns en place – mappage propre des codes, métadonnées typées, retries/hedging bien configurés et observabilité centrée sur les erreurs – vos services gRPC deviennent beaucoup plus résilients, débogables et agréables à consommer pour les autres équipes et langages de votre écosystème.

Damien Larquey

Damien Larquey

Author at Codolie

Passionate about technology, innovation, and sharing knowledge with the developer community.

Back to Blog