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+
- Go :
- Protocol Buffers :
protoc 27+et dépôtgoogleapisà jour pour disposer degoogle/rpc/status.protoetgoogle/rpc/error_details.proto. - Environnement de dev prêt (Go 1.23+, Java 21+, .NET 8/10, etc.).
- Outils de debug :
grpcurlpour invoquer vos RPC depuis la CLI,grpc-health-probepour 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.

É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
Statusriche. - 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 ungoogle.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).
- vérifier que chaque scénario de validation renvoie le bon code (
- 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.
- envoyer une requête invalide :
- 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.
- exposer des métriques type
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
UNKNOWNouINTERNAL:- installer un interceptor/middleware global qui convertit toutes les panics en
INTERNALavec unErrorInfode type « BUG » (stacktrace uniquement en environnement non‑prod), - remapper proprement les erreurs métier connues vers
INVALID_ARGUMENT,FAILED_PRECONDITION, etc.
- installer un interceptor/middleware global qui convertit toutes les panics en
- 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.toStatusRuntimeExceptionen Java), - vérifier la génération des protos
google.rpc.*côté client.
- vérifier que vous utilisez bien les helpers liés au modèle riche (ex :
- Erreurs
UNIMPLEMENTEDinattendues :- souvent dues à des désynchronisations proto (mauvais
packageouservice, vieux stubs clients), - vérifier que les binaires serveur et client sont compilés à partir des mêmes versions de
.proto.
- souvent dues à des désynchronisations proto (mauvais
- 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.
- oublis de gestion de
- 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.reasondocumentés, - une convention pour les champs
metadata(ex :tenant_id,request_id,quota_name).
- un ensemble de
- 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.UNKNOWNen 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_ARGUMENT→400,NOT_FOUND→404,PERMISSION_DENIED→403), - s’assurer que les détails d’erreur restent cohérents entre clients REST et gRPC.
- mapper explicitement les codes gRPC vers les bons codes HTTP (ex :
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
Author at Codolie
Passionate about technology, innovation, and sharing knowledge with the developer community.