Retour au blog

gRPC via proxy dans une architecture microservices : guide complet pour configurer le tunneling HTTP/2

Guide détaillé sur la configuration du trafic gRPC via des serveurs proxy dans une architecture microservices - du tunneling HTTP/2 à l'équilibrage de charge et à la terminaison TLS.

📅7 août 2026
```html

gRPC est un framework RPC haute performance de Google qui fonctionne au-dessus de HTTP/2 et devient le standard de facto pour l'interaction entre services. Mais dès que vous essayez de faire passer le trafic gRPC à travers un proxy, des problèmes surgissent immédiatement : la plupart des serveurs proxy classiques ne savent pas gérer HTTP/2 et les connexions de streaming à long terme. Dans cet article, nous allons examiner comment configurer correctement gRPC via un proxy — du choix de l'architecture aux configurations spécifiques de Nginx, Envoy et HAProxy.

Pourquoi gRPC fonctionne mal avec les proxies classiques

Pour comprendre le problème, il faut examiner comment gRPC est conçu en interne. Le protocole utilise HTTP/2 comme couche de transport, ce qui le différencie radicalement du REST habituel sur HTTP/1.1. La plupart des proxies d'entreprise, des pare-feu et des répartiteurs de charge ont été conçus à l'époque de HTTP/1.1 et ne savent tout simplement pas gérer les flux multiplexés de HTTP/2.

Voici quelques problèmes spécifiques que vous rencontrerez en essayant de faire passer gRPC à travers un proxy HTTP classique :

  • Downgrade à HTTP/1.1. De nombreux proxies rétrogradent automatiquement la version du protocole. gRPC nécessite HTTP/2 — sans cela, la connexion ne pourra tout simplement pas être établie, et le client recevra une erreur UNAVAILABLE.
  • Interruption des connexions à long terme. gRPC utilise activement le streaming serveur et bidirectionnel. Les proxies avec des délais d'attente agressifs (en particulier AWS ELB Classic, certaines versions de Squid) interrompent les connexions qui ne transmettent pas de données pendant plus de 60 secondes.
  • Problèmes de Content-Type. gRPC utilise l'en-tête Content-Type: application/grpc. Un proxy qui ne connaît pas ce type peut rejeter la requête ou mettre en mémoire tampon le corps de manière incorrecte.
  • Trailers. gRPC utilise les trailers HTTP/2 pour transmettre le statut de fin d'appel. Les proxies HTTP/1.1 ne prennent pas en charge les trailers — l'information sur le statut sera perdue.
  • Mise en mémoire tampon du corps. Certains proxies mettent en mémoire tampon tout le corps de la réponse avant de le transmettre au client. Pour les appels gRPC en streaming, cela signifie que le client ne recevra aucun message tant que le flux ne sera pas terminé.

Conclusion clé :

Pour faire fonctionner gRPC via un proxy, il faut un proxy prenant en charge HTTP/2 de bout en bout ou un mode de tunneling spécial. Un proxy HTTP classique sans configuration supplémentaire ne conviendra pas.

Tunneling HTTP/2 : comment ça fonctionne

Il existe deux approches fondamentalement différentes pour le proxying du trafic gRPC, et il est important de comprendre la différence entre elles pour choisir la bonne solution pour votre architecture.

Approche 1 : HTTP/2 de bout en bout (recommandée)

Le proxy comprend HTTP/2 et établit une connexion HTTP/2 à la fois avec le client et avec le backend. Il peut analyser des flux individuels, appliquer une répartition de charge au niveau des requêtes, ajouter des en-têtes et effectuer une terminaison TLS. C'est l'option la plus fonctionnelle — c'est ainsi que fonctionnent Envoy, Nginx (à partir de la version 1.13.10) et les répartiteurs de charge compatibles gRPC chez les fournisseurs de cloud.

Approche 2 : Tunneling TCP (CONNECT)

Le proxy n'analyse pas le trafic HTTP/2, mais crée simplement un tunnel TCP transparent via la méthode CONNECT. Le client établit une connexion TLS directement avec le backend via le tunnel. Le proxy ne voit que le flux de bytes chiffrés. Cette méthode est plus simple à configurer, mais vous prive des possibilités de répartition de charge au niveau des requêtes gRPC et d'ajout d'en-têtes.

Caractéristique HTTP/2 de bout en bout Tunnel TCP CONNECT
Répartition par requêtes ✅ Oui ❌ Non (uniquement par TCP)
Terminaison TLS sur le proxy ✅ Oui ❌ Non
Ajout d'en-têtes ✅ Oui ❌ Non
Complexité de configuration Moyenne Faible
Support du streaming ✅ Complet ✅ Complet
Observabilité (métriques) ✅ Détail ❌ Seulement TCP

Configurer Nginx comme proxy gRPC

Nginx prend en charge le proxying gRPC à partir de la version 1.13.10 (février 2018). Pour cela, le module ngx_http_grpc_module est nécessaire, qui fait partie de la distribution standard. Il est important de noter que Nginx prend en charge HTTP/2 côté client (frontend), mais utilise HTTP/2 côté backend uniquement pour gRPC — le upstream HTTP classique fonctionne sur HTTP/1.1.

Configuration de base du proxy gRPC sur Nginx

server {
    listen 443 ssl http2;
    server_name grpc.example.com;

    # Certificats TLS
    ssl_certificate     /etc/nginx/ssl/server.crt;
    ssl_certificate_key /etc/nginx/ssl/server.key;

    # Paramètres TLS modernes
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;

    location / {
        # Directive grpc_pass au lieu de proxy_pass
        grpc_pass grpc://grpc_backend;

        # Délais d'attente pour les flux à long terme
        grpc_read_timeout  3600s;
        grpc_send_timeout  3600s;
        grpc_connect_timeout 5s;

        # Transmission de l'IP réelle du client
        grpc_set_header X-Real-IP $remote_addr;
        grpc_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

upstream grpc_backend {
    server backend-1:50051;
    server backend-2:50051;
    server backend-3:50051;

    # Keepalive pour les connexions HTTP/2
    keepalive 32;
}

Faites attention à quelques détails critiques. Tout d'abord, la directive grpc_pass est utilisée au lieu de proxy_pass — ce sont des modules différents avec des comportements différents. Deuxièmement, les délais d'attente grpc_read_timeout et grpc_send_timeout sont fixés à 3600 secondes (1 heure) — c'est important pour les flux serveur qui peuvent fonctionner longtemps sans transmettre de données. Troisièmement, keepalive 32 dans le upstream permet de réutiliser les connexions HTTP/2 avec les backends.

Configuration pour gRPC non chiffré (grpc://)

server {
    listen 80 http2;
    server_name grpc-internal.example.com;

    location / {
        grpc_pass grpc://127.0.0.1:50051;

        # Gestion des erreurs gRPC
        error_page 502 = /error502grpc;
    }

    location = /error502grpc {
        internal;
        default_type application/grpc;
        add_header grpc-status 14;
        add_header content-length 0;
        return 204;
    }
}

Le bloc error502grpc est un détail important : en cas d'indisponibilité du backend, il retourne le statut gRPC correct UNAVAILABLE (14) au lieu de HTTP 502, que le client gRPC ne pourra pas traiter correctement.

Envoy Proxy : le meilleur choix pour gRPC dans les microservices

Envoy a été créé par Lyft spécifiquement pour l'architecture des microservices, et le support de gRPC y est implémenté à un niveau très profond. Il comprend les Protocol Buffers, peut transcoder gRPC en REST, collecte des métriques détaillées pour chaque méthode RPC et constitue la base des solutions de service mesh — Istio, AWS App Mesh et d'autres. Si vous construisez une architecture de microservices sérieuse, Envoy est le standard de l'industrie.

Configuration de base d'Envoy pour gRPC

static_resources:
  listeners:
  - name: grpc_listener
    address:
      socket_address:
        address: 0.0.0.0
        port_value: 8080
    filter_chains:
    - filters:
      - name: envoy.filters.network.http_connection_manager
        typed_config:
          "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
          stat_prefix: grpc_proxy
          codec_type: HTTP2
          route_config:
            name: grpc_routes
            virtual_hosts:
            - name: grpc_services
              domains: ["*"]
              routes:
              - match:
                  prefix: "/com.example.UserService"
                route:
                  cluster: user_service
                  timeout: 30s
                  retry_policy:
                    retry_on: "reset,connect-failure,retriable-status-codes"
                    num_retries: 3
                    retriable_status_codes: [14]
              - match:
                  prefix: "/com.example.OrderService"
                route:
                  cluster: order_service
                  timeout: 60s
          http_filters:
          - name: envoy.filters.http.grpc_stats
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.filters.http.grpc_stats.v3.FilterConfig
              emit_filter_state: true
          - name: envoy.filters.http.router
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router

  clusters:
  - name: user_service
    connect_timeout: 5s
    type: STRICT_DNS
    lb_policy: ROUND_ROBIN
    http2_protocol_options: {}
    load_assignment:
      cluster_name: user_service
      endpoints:
      - lb_endpoints:
        - endpoint:
            address:
              socket_address:
                address: user-service
                port_value: 50051

Les principaux avantages de cette configuration : routage par services gRPC (le préfixe du chemin correspond au nom complet du service au format package.ServiceName), réessais automatiques en cas de statut UNAVAILABLE et collecte de métriques gRPC via le filtre grpc_stats.

Transcodage gRPC-Web dans Envoy

L'une des fonctionnalités clés d'Envoy est le transcoder gRPC-Web intégré. Les navigateurs ne prennent pas en charge gRPC directement (en raison des limitations de l'API Fetch sur les trailers HTTP/2), donc le protocole gRPC-Web est utilisé. Envoy peut automatiquement convertir les requêtes gRPC-Web du navigateur en gRPC normal pour le backend :

http_filters:
- name: envoy.filters.http.grpc_web
  typed_config:
    "@type": type.googleapis.com/envoy.extensions.filters.http.grpc_web.v3.GrpcWeb
- name: envoy.filters.http.cors
  typed_config:
    "@type": type.googleapis.com/envoy.extensions.filters.http.cors.v3.CorsPolicy
- name: envoy.filters.http.router
  typed_config:
    "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router

HAProxy et gRPC : configuration avec répartition de charge

HAProxy prend en charge gRPC à partir de la version 1.9.2. Il fonctionne au niveau TCP ou HTTP/2, peut équilibrer le trafic gRPC et effectuer des vérifications de santé. HAProxy est un bon choix si vous l'utilisez déjà dans votre infrastructure et souhaitez ajouter le support gRPC sans introduire un nouveau composant.

global
    maxconn 50000
    log stdout format raw local0

defaults
    log global
    timeout connect 5s
    timeout client  3600s
    timeout server  3600s

frontend grpc_frontend
    bind *:443 ssl crt /etc/haproxy/certs/server.pem alpn h2,http/1.1
    mode http
    option http-use-htx
    default_backend grpc_servers

backend grpc_servers
    mode http
    balance leastconn
    option http-use-htx

    # Vérification de santé via le protocole gRPC Health Checking
    option httpchk GET /grpc.health.v1.Health/Check
    http-check expect status 200

    server grpc1 10.0.0.1:50051 check ssl verify none
    server grpc2 10.0.0.2:50051 check ssl verify none
    server grpc3 10.0.0.3:50051 check ssl verify none

Faites attention à quelques paramètres importants. alpn h2,http/1.1 dans la directive bind indique que HAProxy accepte à la fois les connexions HTTP/2 et HTTP/1.1 via la négociation TLS ALPN. timeout client 3600s et timeout server 3600s sont des paramètres critiques pour les flux gRPC à long terme. L'algorithme leastconn est préférable à roundrobin pour gRPC, car les connexions de streaming peuvent être à long terme et déséquilibrer les backends.

Terminaison TLS et chiffrement de bout en bout pour gRPC

gRPC suppose par défaut l'utilisation de TLS — c'est une partie de la spécification. En pratique, dans une architecture de microservices, la question se pose : où effectuer la terminaison TLS et comment organiser le chiffrement entre les composants ? Il existe trois modèles principaux.

Modèle 1 : Terminaison TLS sur le proxy (Edge TLS)

Le proxy accepte le trafic chiffré des clients, le déchiffre et le transmet aux backends par un canal non chiffré (ou avec un TLS séparé). C'est l'approche la plus courante dans les réseaux d'entreprise. Les backends peuvent utiliser grpc.Insecure() pour simplifier la configuration.

Modèle 2 : Chiffrement de bout en bout (mTLS)

Le TLS mutuel (mTLS) est la norme pour les services mesh. Chaque service a son propre certificat, et à chaque connexion, les deux parties vérifient les certificats de l'autre. Cela assure l'authentification des services et le chiffrement du trafic à l'intérieur du cluster. C'est ainsi qu'Istio fonctionne avec le proxy sidecar Envoy.

# Go : serveur gRPC avec mTLS
import (
    "crypto/tls"
    "crypto/x509"
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials"
)

func createMTLSCredentials() credentials.TransportCredentials {
    cert, _ := tls.LoadX509KeyPair("server.crt", "server.key")
    
    caCert, _ := os.ReadFile("ca.crt")
    caCertPool := x509.NewCertPool()
    caCertPool.AppendCertsFromPEM(caCert)
    
    tlsConfig := &tls.Config{
        Certificates: []tls.Certificate{cert},
        ClientAuth:   tls.RequireAndVerifyClientCert,
        ClientCAs:    caCertPool,
    }
    
    return credentials.NewTLS(tlsConfig)
}

// Création d'un serveur avec mTLS
creds := createMTLSCredentials()
server := grpc.NewServer(grpc.Creds(creds))

Modèle 3 : TLS Passthrough

Le proxy fonctionne au niveau TCP et ne déchiffre pas le trafic — il redirige simplement les bytes chiffrés vers le backend. C'est l'option la plus simple du point de vue du proxy, mais elle prive de la possibilité d'analyser le trafic gRPC, d'ajouter des en-têtes ou d'effectuer une répartition de charge au niveau des requêtes.

Répartition de charge pour les flux gRPC : particularités et solutions

La répartition de charge pour gRPC est une tâche non triviale, et voici pourquoi. Dans HTTP/1.1, chaque requête est une connexion TCP distincte (ou une connexion d'un pool), et le répartiteur de charge répartit facilement les requêtes entre les backends. Dans HTTP/2, une seule connexion TCP multiplexe de nombreux flux — si le répartiteur de charge fonctionne au niveau TCP, tous les flux d'une connexion iront vers un seul backend.

Pour gRPC, cela signifie que si le client établit une seule connexion HTTP/2 et envoie 100 requêtes RPC à travers celle-ci, avec une répartition TCP, toutes les 100 requêtes seront traitées par une seule instance de backend. Les autres backends resteront inactifs. La solution est de répartir la charge au niveau des flux HTTP/2 (répartition L7).

Algorithmes de répartition pour gRPC

Algorithme Adapté pour gRPC Commentaire
Round Robin ✅ Oui Bon pour les RPC unaires avec des temps de traitement à peu près égaux
Least Connection ✅ Meilleur choix Prend en compte les flux actifs, répartit la charge de manière uniforme
Random ⚠️ Conditionnel Fonctionne avec un grand nombre de requêtes, déséquilibré avec peu
IP Hash ❌ Mauvais Lie le client à un seul backend, pas de sens en L7
Pick First (intégré à gRPC) ❌ Pas pour la production Toutes les requêtes vont au premier serveur disponible

Répartition de charge côté client dans gRPC

gRPC prend en charge la répartition de charge côté client — le client décide lui-même sur quel serveur envoyer la requête. Cela permet de contourner le problème de multiplexage TCP. Pour la découverte de services, DNS est utilisé avec plusieurs enregistrements A ou des résolveurs spéciaux (Consul, etcd). Exemple de configuration de la répartition de charge côté client en Go :

import (
    "google.golang.org/grpc"
    "google.golang.org/grpc/balancer/roundrobin"
)

// Client avec répartition round-robin via DNS
conn, err := grpc.Dial(
    "dns:///grpc-service.internal:50051",
    grpc.WithDefaultServiceConfig(
        `{"loadBalancingConfig": [{"round_robin":{}}]}`
    ),
    grpc.WithTransportCredentials(creds),
)

// Client avec least-connection (disponible dans gRPC >= 1.58)
conn, err := grpc.Dial(
    "dns:///grpc-service.internal:50051",
    grpc.WithDefaultServiceConfig(
        `{"loadBalancingConfig": [{"least_request":{}}]}`
    ),
    grpc.WithTransportCredentials(creds),
)

Proxies externes pour gRPC : quand et pourquoi en avoir besoin dans les microservices

En plus des composants proxy internes (Nginx, Envoy, HAProxy), il arrive parfois dans une architecture de microservices qu'il soit nécessaire d'utiliser des serveurs proxy externes — par exemple, pour router le trafic à travers des régions spécifiques, contourner des restrictions réseau ou isoler des connexions sortantes. Examinons les principaux scénarios.

Scénario 1 : Microservices géo-distribués

Si vos microservices sont situés dans différentes régions (par exemple, une partie en Europe, une partie aux États-Unis), et que vous devez contrôler par quelle adresse IP les connexions gRPC sont établies entre les régions, des proxies externes peuvent aider à organiser un routage prévisible. Pour de telles tâches, des proxies de centre de données sont adaptés — ils fournissent des adresses IP stables et une vitesse de connexion élevée, ce qui est critique pour gRPC avec sa sensibilité aux latences.

Scénario 2 : Isolation du trafic sortant

Dans certains environnements d'entreprise, tout le trafic sortant doit passer par un proxy d'entreprise. Pour les clients gRPC qui doivent accéder à des services gRPC externes (par exemple, les API Google Cloud qui utilisent gRPC), cela crée des difficultés. La solution consiste à configurer un tunnel CONNECT via le proxy d'entreprise.

Exemple de configuration d'un client gRPC pour fonctionner via un proxy HTTP CONNECT en Go :

import (
    "net"
    "net/http"
    "golang.org/x/net/proxy"
    "google.golang.org/grpc"
)

// Utilisation d'un proxy SOCKS5 pour gRPC
proxyDialer, _ := proxy.SOCKS5(
    "tcp",
    "proxy.example.com:1080",
    &proxy.Auth{User: "user", Password: "pass"},
    proxy.Direct,
)

conn, err := grpc.Dial(
    "grpc-service.example.com:443",
    grpc.WithContextDialer(func(ctx context.Context, addr string) (net.Conn, error) {
        return proxyDialer.Dial("tcp", addr)
    }),
    grpc.WithTransportCredentials(creds),
)

Scénario 3 : Test et développement

Lors du développement de microservices, il est souvent nécessaire de tester le comportement des services depuis différents environnements réseau — vérifier comment un service répond en cas de latence élevée, ou tester une logique dépendante de la géographie. Pour de telles tâches, des proxies résidentiels sont pratiques, car ils permettent de simuler des requêtes depuis des régions spécifiques avec de vraies adresses IP d'utilisateurs domestiques.

Erreurs typiques lors de la configuration de gRPC via un proxy et leur résolution

Examinons les problèmes les plus fréquents rencontrés par les développeurs lors de la configuration de gRPC via un proxy et les moyens spécifiques de les résoudre.

Erreur 1 : "transport: received the unexpected content-type"

Symptôme :

Le client reçoit l'erreur transport: received the unexpected content-type "text/html; charset=utf-8"

Cause : Le proxy a renvoyé une page HTML avec une erreur (par exemple, 502 Bad Gateway) au lieu d'une réponse gRPC. Le client gRPC ne sait pas traiter le HTML et génère cette erreur.
Solution : Assurez-vous que le backend est accessible. Ajoutez la gestion des erreurs gRPC au niveau du proxy (comme montré dans l'exemple Nginx ci-dessus avec le bloc error502grpc).

Erreur 2 : La connexion se coupe après 60-120 secondes

Symptôme :

Les connexions gRPC en streaming se ferment de manière inattendue avec l'erreur UNAVAILABLE: transport is closing après un intervalle de temps à peu près égal.

Cause : Le proxy ferme les connexions inactives après un délai d'attente. Valeurs classiques : AWS ELB — 60 secondes, Nginx par défaut — 60 secondes.
Solution 1 : Augmentez les délais d'attente sur le proxy (comme montré dans les exemples ci-dessus).
Solution 2 : Configurez le keepalive côté client gRPC :

import "google.golang.org/grpc/keepalive"

kaParams := keepalive.ClientParameters{
    Time:                30 * time.Second, // Ping toutes les 30 secondes
    Timeout:             10 * time.Second, // Attendre une réponse pendant 10 secondes
    PermitWithoutStream: true,             // Pinguer même sans RPC actifs
}

conn, err := grpc.Dial(
    "grpc-service:50051",
    grpc.WithKeepaliveParams(kaParams),
    grpc.WithTransportCredentials(creds),
)

Erreur 3 : HTTP/2 ne s'accorde pas (échec ALPN)

Symptôme :

Erreur transport: failed to dial: context deadline exceeded ou no application protocol

Cause : Le proxy ou le matériel intermédiaire ne prend pas en charge ALPN (Application-Layer Protocol Negotiation) ou h2 n'est pas inclus dans la liste des protocoles pris en charge.
Solution : Assurez-vous que dans la configuration TLS du proxy, le protocole h2 est explicitement indiqué : alpn h2,http/1.1 (HAProxy) ou listen 443 ssl http2 (Nginx).

Erreur 4 : Le streaming se fige — les données n'arrivent pas

Symptôme :

Le streaming serveur fonctionne sur le backend (visible dans les logs), mais le client ne reçoit pas de messages jusqu'à la fin du streaming.

Cause : Le proxy met en mémoire tampon la réponse et l'envoie au client uniquement après la fin. Problème typique pour les proxies configurés avec proxy_buffering on.
Solution pour Nginx :

location / {
    grpc_pass grpc://backend;
    
    # Désactiver la mise en mémoire tampon pour les flux gRPC
    grpc_buffer_size 0;
    
    # Ou pour un proxy_pass classique :
    proxy_buffering off;
    proxy_cache off;
}

Liste de contrôle de diagnostic pour gRPC via proxy

✅ Liste de contrôle : diagnostic gRPC-proxy

  • Le proxy prend en charge HTTP/2 (vérifiez avec curl --http2 -v)
  • ALPN h2 est activé dans les configurations TLS du proxy
  • Les délais d'attente client et serveur sont fixés à au moins 300 secondes
  • La mise en mémoire tampon des réponses est désactivée pour les points de terminaison gRPC
  • Le keepalive gRPC est configuré sur le client (Time: 30s, Timeout: 10s)
  • Les erreurs du backend sont renvoyées sous forme de statuts gRPC, et non de codes HTTP
  • La vérification de santé utilise le protocole gRPC Health Checking
  • La répartition fonctionne en L7 (flux HTTP/2), et non en L4 (TCP)

Conclusion

La configuration de gRPC via un proxy nécessite de comprendre les différences clés entre HTTP/2 et HTTP/1.1 : multiplexage des flux, connexions à long terme, trailers HTTP/2 et négociation ALPN. Les proxies HTTP classiques sans configuration spéciale ne fonctionnent pas avec gRPC — il faut soit un proxy L7 prenant en charge HTTP/2 (Nginx 1.13.10+, Envoy, HAProxy 1.9.2+), soit un tunneling TCP CONNECT.

Pour un environnement de production, Envoy est recommandé — il a été conçu spécifiquement pour l'architecture des microservices, a un support natif pour gRPC, peut collecter des métriques détaillées pour chaque méthode RPC et constitue la base de la plupart des solutions de service mesh. Nginx est un bon choix si vous l'utilisez déjà comme API Gateway et souhaitez ajouter le support gRPC sans introduire un nouveau composant. HAProxy conviendra si la performance est critique et que vous êtes déjà familier avec sa configuration.

Quelle que soit la solution proxy choisie, trois règles demeurent inchangées : augmentez les délais d'attente pour les flux à long terme, désactivez la mise en mémoire tampon des réponses et configurez le keepalive côté client. Ces trois réglages résolvent 80 % des problèmes avec gRPC via un proxy.

Si dans votre architecture, les services gRPC doivent interagir à travers des réseaux externes ou si vous avez besoin de router le trafic à travers des régions spécifiques, envisagez des proxies de centre de données — ils fournissent des adresses IP stables, une faible latence et une grande capacité, ce qui est particulièrement important pour gRPC avec son protocole binaire et sa sensibilité à la latence.

```