Protobuf FieldMask : laissez vos clients d'API ne demander que ce dont ils ont besoin
3 juillet 2026 · 7 min read · Read in English
Sommaire
Protobuf FieldMask : laissez vos clients d'API ne demander que ce dont ils ont besoin
Un endpoint est rarement utilisé par un seul client. Une app mobile, un front web, un job batch et un autre service backend tapent tous le même GetProduct et ils ne veulent pas la même chose. L'app mobile veut un titre et un prix. La page produit veut aussi les avis et les recommandations. Le batch ne veut qu'un ID et une date.
Le problème : si le serveur calcule tout à chaque fois, l'appelant le moins gourmand paie le prix du plus gourmand. Certains champs sont gratuits (ils sont déjà dans la ligne qu'on a lue). D'autres non : ils veulent dire une agrégation SQL, un appel à un autre service, ou un gros payload sur une connexion mobile lente.
FieldMask est une manière standard et minimale pour le client de dire « ne me renvoie que ces champs », et pour le serveur de sauter le travail derrière les champs que personne n'a demandés. Cet article explique l'idée avec une API de boutique en ligne classique, pas une recette à copier-coller, pour que vous puissiez l'appliquer à vos propres services.
Un scénario classique
Imaginons une boutique en ligne. Votre service produit expose une méthode gRPC :
service ProductService {
rpc GetProduct(GetProductRequest) returns (Product);
}
message Product {
string id = 1;
string title = 2;
Money price = 3;
ReviewSummary reviews = 4; // agrégation sur la table des avis
repeated Product recommendations = 5; // appel au service de reco
StockInfo stock = 6; // fan-out vers chaque entrepôt
}
Regardez le coût de chaque champ :
| Champ | Coût pour le produire |
|---|---|
id, title, price | gratuit, déjà dans la ligne produit |
reviews | un GROUP BY sur des millions de lignes |
recommendations | un appel distant au service de recommandation |
stock | un fan-out vers chaque entrepôt, puis une somme |
Un téléphone qui affiche un résultat de recherche n'a besoin que de title et price. Mais si GetProduct construit toujours le Product complet, ce téléphone déclenche une agrégation d'avis, un appel de reco et un fan-out de stock qu'il n'affichera jamais. Multipliez par des milliers de requêtes par seconde et vous brûlez du CPU, vous saturez les services en aval, et vous envoyez des octets que personne ne lit.
La solution tentante qui ne passe pas à l'échelle
Le premier réflexe, ce sont les booléens :
message GetProductRequest {
string id = 1;
bool include_reviews = 2;
bool include_recommendations = 3;
bool include_stock = 4;
}
Ça marche, jusqu'à ce que ça ne marche plus. Chaque nouveau champ coûteux ajoute un flag include_*. Les champs imbriqués (je veux reviews.average mais pas le détail complet) n'ont aucune expression propre. Et chaque client doit connaître votre liste privée de flags. C'est un casse-tête combinatoire qui grossit avec votre schéma.
Ce qu'on veut, c'est un seul mécanisme qui passe à l'échelle sur n'importe quel champ, s'imbrique naturellement, et fait partie de la boîte à outils standard. C'est FieldMask.
Ce qu'est réellement FieldMask
google.protobuf.FieldMask est un type bien connu (well-known type) livré avec Protocol Buffers. C'est presque rien : juste une liste de chemins de champs :
message FieldMask {
repeated string paths = 1;
}
Un chemin est un nom de champ, séparé par des points pour l'imbrication : title, price, reviews.average. C'est tout. Le client le remplit, le serveur le lit. C'est le cousin gRPC du ?fields=title,price de REST et de la sélection de champs de GraphQL, un jeu de champs partiel (sparse fieldset) : demandez un sous-ensemble, recevez un sous-ensemble.
Vous ajoutez un champ à votre requête :
message GetProductRequest {
string id = 1;
google.protobuf.FieldMask field_mask = 2;
}
Et le client demande exactement ce dont il a besoin :
field_mask {
paths: "title"
paths: "price"
}
Lire avec un masque
Voici le flux. Le masque fait deux tâches distinctes, et il ne faut pas les confondre :
Client ── field_mask { title, price } ──► Serveur
│
│ 1. GARDE : ne fait le travail
│ coûteux que pour les champs demandés
│ reviews ? pas demandé → skip
│ recos ? pas demandé → skip
│ stock ? pas demandé → skip
│
│ 2. FILTRE : réduit la réponse
│ aux seuls chemins demandés
▼
Client ◄── Product { title, price } ── Serveur
Tâche 1 : garder le travail coûteux. C'est celle qui fait économiser, et le serveur doit la faire à la main. FieldMask ne saute pas votre code par magie ; il vous dit seulement ce qu'il est sûr de sauter. En Go :
func (s *server) GetProduct(ctx context.Context, req *pb.GetProductRequest) (*pb.Product, error) {
paths := req.GetFieldMask().GetPaths()
wants := func(f string) bool { return len(paths) == 0 || slices.Contains(paths, f) }
p, err := s.loadProductRow(ctx, req.GetId()) // pas cher : id, title, price
if err != nil {
return nil, err
}
if wants("reviews") {
p.Reviews = s.aggregateReviews(ctx, p.Id) // coûteux, seulement si demandé
}
if wants("recommendations") {
p.Recommendations = s.fetchRecommendations(ctx, p.Id) // appel distant
}
if wants("stock") {
p.Stock = s.fanOutStock(ctx, p.Id) // fan-out
}
return p, nil
}
Notez le repli len(paths) == 0 : un masque vide veut dire « tout ». Un client qui ne connaît pas ou se moque de FieldMask continue de fonctionner exactement comme avant. C'est ce qui rend la fonctionnalité sûre à ajouter sur une API existante.
Tâche 2 : filtrer la réponse. Même après la garde, un champ peut être rempli sans que le client l'ait demandé (valeurs par défaut, valeurs en cache, champ gratuit à calculer). Réduire le message au masque garde le contrat honnête et le payload léger. Le runtime protobuf fournit un utilitaire pour ça : en Java c'est FieldMaskUtil.merge(), la plupart des langages ont un équivalent. Appliqué sur la réponse construite, il retire tout chemin absent du masque.
L'écriture, c'est la même idée
FieldMask n'est pas que pour la lecture. Sur une mise à jour, le même masque répond à une autre question, « quels champs ai-je le droit de modifier ? », c'est exactement ce que veut dire PATCH en REST :
message UpdateProductRequest {
Product product = 1;
google.protobuf.FieldMask update_mask = 2;
}
Avec update_mask { paths: "price" }, le serveur ne touche qu'au prix et laisse tout le reste intact. Mais l'écriture a ses propres règles : effacer un champ ou le laisser tel quel, les chemins imbriqués, et le danger d'un masque vide (qui, en écriture, veut dire tout remplacer, exactement l'inverse de la lecture). C'est un sujet à part entière, traité dans la suite : Protobuf FieldMask pour l'écriture →.
Les pièges
- Les noms de champs deviennent une partie de votre contrat d'API. Sur le fil, protobuf identifie les champs par numéro, donc renommer un champ est normalement sans risque. Avec FieldMask, le client envoie le nom du champ sous forme de chaîne, donc renommez
reviewsenreview_summaryet tous les masques existants cassent silencieusement. Règle : ne renommez pas un champ masqué. Si vous devez le faire, continuez d'accepter l'ancien nom côté serveur jusqu'à ce que tous les appelants aient migré, puis dépréciez-le. - Le masque ne saute rien tout seul. Ce n'est qu'une liste de chaînes. Si vous oubliez les gardes
if wants(...), vous calculez tout et vous ne gagnez rien. Les économies vivent dans votre code, pas dans le framework. - Les champs répétés ne se sous-sélectionnent pas. Vous pouvez demander
recommendations, mais pasrecommendations.title, car le FieldMask de protobuf n'a pas de syntaxe pour « ce sous-champ, dans chaque élément d'une liste ». Connaissez la limite avant de concevoir autour. - Livrez des masques par défaut sensés. N'obligez pas chaque client à écrire ses chemins à la main. Exposez des masques prêts à l'emploi dans votre bibliothèque cliente pour les cas courants (
ProductMasks.SUMMARY,ProductMasks.FULL), pour que les appelants choisissent un jeu soigné plutôt que de deviner des noms de champs.
Récapitulatif
| Sans FieldMask | Avec FieldMask |
|---|---|
| Un endpoint calcule tout pour tout le monde | Chaque appelant déclare les champs qu'il veut |
| Les appelants légers paient les champs coûteux | Le travail coûteux ne tourne que sur demande |
Un flag include_* par champ coûteux | Un seul mécanisme, tout champ, imbrication incluse |
| PATCH ambigu (vide = effacer ou ignorer ?) | Le masque de mise à jour dit exactement quoi changer |
FieldMask est un ajout minuscule (un champ sur la requête) qui transforme un endpoint rigide et uniforme en un endpoint flexible, sans explosion de flags et sans casser les clients existants (masque vide = tout). Le hic : ça ne paie que si vous branchez les gardes vous-même. Le masque vous dit ce que vous avez le droit de sauter, mais sauter le travail reste votre boulot.
S'abonner aux prochains articles
Recevez les nouveaux articles par e-mail. Pas de spam, désinscription à tout moment.
Related posts
Protobuf FieldMask pour l'écriture : un seul endpoint d'update, zéro ambiguïté
Une seule méthode Update qui ne modifie que les champs nommés, sans endpoint par champ ni écrasement du reste. Comment l'update_mask apporte une sémantique PATCH propre à gRPC : mettre à jour, effacer, chemins imbriqués, et le piège du masque vide qui peut effacer des données en silence.
July 7, 2026
Pourquoi le rate limiting sur Redis casse à grande échelle (et ce que fait Uber à la place)
Un token bucket en mémoire, c'est trivial. Mettez-le derrière Redis et ça marche, jusqu'à ce que ça ne marche plus. On part du rate limiting sur un seul nœud, puis sur un Redis partagé, on voit pourquoi ce modèle s'effondre à des millions de requêtes par seconde, et le virage qu'a pris Uber : appliquer localement, coordonner globalement, et rejeter par probabilité.
July 10, 2026
Retry Storms : comment de bons clients font tomber des serveurs en bonne santé
Un retry semble inoffensif : la requête a échoué, on réessaie. Multipliez ça par chaque client, ajoutez une dépendance lente, et les retries deviennent un DDoS auto-infligé. On part de la boucle de retry naïve vers le backoff exponentiel, le jitter, les retry budgets et les circuit breakers, la moitié côté client de la résilience, qui complète le rate limiting côté serveur.
July 11, 2026