Protocole gRPC-Connect : Communication frontend-backend unifiée pour les microservices Go 2026

技术架构

Introduction : Pourquoi la communication frontend-backend de vos microservices est-elle encore un chaos

Nous sommes en 2026. Si vos microservices Go utilisent encore REST + JSON et maintiennent manuellement deux ensembles de définitions d'API (Protobuf backend + TypeScript frontend), vous êtes profondément dans le bourbier suivant : le backend modifie un champ et le frontend n'est pas au courant, la documentation Swagger est toujours obsolète, la communication en streaming nécessite des contournements avec WebSocket, les codes d'erreur sont définis séparément de chaque côté...

Le protocole gRPC-Connect met fin à tout cela. Il définit un ensemble de services basé sur Protobuf, le backend utilise gRPC pour une communication haute performance et le frontend appelle de manière transparente en utilisant le mode HTTP/JSON de Connect-RPC. Un Proto, les deux côtés fonctionnent. La maturité de l'écosystème Buf rend cela plus simple que jamais.

Cet article vous guide dans la construction de 5 modèles essentiels gRPC-Connect en Go, couvrant toute la chaîne de la définition de services à la passerelle prête pour la production.

Concepts clés en un coup d'œil

Concept Description Outils de l'écosystème
gRPC-Connect Protocole RPC basé sur HTTP/2 prenant en charge les protocoles gRPC et Connect connect-go
Connect-RPC Implémentation Go du protocole Connect, prenant en charge les modes gRPC/gRPC-Web/Connect connectrpc.com
Buf Toolchain de construction Protobuf, remplaçant protoc buf.build
gRPC-Web Protocole gRPC côté navigateur connect-web
Protobuf Langage de définition d'interface google.golang.org/protobuf
Streaming Streaming Serveur/Client/Bidirectionnel connect-go
Intercepteur Mécanisme de middleware Connect connect-go

Cinq points de douleur : Pourquoi la communication frontend-backend traditionnelle ne suit plus

Point de douleur 1 : Le cauchemar de la maintenance de doubles définitions d'API. Le backend définit des services gRPC avec Protobuf, le frontend définit des API REST avec des interfaces TypeScript — deux ensembles de définitions qui ne sont jamais synchronisés.

Point de douleur 2 : La documentation Swagger est inutile. La documentation OpenAPI maintenue manuellement est toujours en retard sur le code réel. Les développeurs frontend déboguent constamment avec une documentation obsolète.

Point de douleur 3 : Solutions de streaming fragmentées. Le streaming gRPC n'est pas disponible dans les navigateurs, forçant l'adoption de WebSocket et menant à une fragmentation du protocole.

Point de douleur 4 : Gestion des erreurs incohérente. gRPC utilise des Status Codes, REST utilise des codes d'état HTTP — le frontend a besoin de deux ensembles de logique de gestion des erreurs.

Point de douleur 5 : Toolchain de génération de code chaotique. Conflits de versions de plugins protoc, styles de code généré incohérents et intégration CI/CD difficile.

Modèle 1 : Définition de services Connect-RPC

Définissez des services en utilisant Buf et Connect-RPC — un Proto génère à la fois le code backend Go et le code frontend TypeScript.

// Runtime: Buf v1.47+, connect-go v1.18+, protoc-gen-go v1.34+
// File: proto/order/v1/order.proto

syntax = "proto3";

package order.v1;

option go_package = "github.com/example/gen/order/v1;orderv1";

service OrderService {
  rpc CreateOrder(CreateOrderRequest) returns (CreateOrderResponse) {}
  rpc GetOrder(GetOrderRequest) returns (GetOrderResponse) {}
  rpc StreamOrders(StreamOrdersRequest) returns (stream Order) {}
  rpc UploadOrders(stream UploadOrderRequest) returns (UploadOrdersResponse) {}
  rpc OrderChat(stream ChatMessage) returns (stream ChatMessage) {}
}

message CreateOrderRequest {
  string user_id = 1;
  repeated OrderItem items = 2;
  string shipping_address = 3;
}

message CreateOrderResponse {
  string order_id = 1;
  string status = 2;
  int64 created_at = 3;
}

message GetOrderRequest {
  string order_id = 1;
}

message GetOrderResponse {
  Order order = 1;
}

message Order {
  string order_id = 1;
  string user_id = 2;
  repeated OrderItem items = 3;
  string status = 4;
  int64 created_at = 5;
  int64 updated_at = 6;
}

message OrderItem {
  string product_id = 1;
  string product_name = 2;
  int32 quantity = 3;
  double price = 4;
}

message StreamOrdersRequest {
  string user_id = 1;
}

message UploadOrderRequest {
  string user_id = 1;
  repeated OrderItem items = 2;
}

message UploadOrdersResponse {
  int32 total_created = 1;
  repeated string order_ids = 2;
}

message ChatMessage {
  string sender = 1;
  string message = 2;
  int64 timestamp = 3;
}
# File: buf.yaml
version: v2
modules:
  - path: proto
    name: buf.build/example/orders
lint:
  use:
    - STANDARD
breaking:
  use:
    - FILE
# File: buf.gen.yaml
version: v2
managed:
  enabled: true
  override:
    - file_option: go_package_prefix
      value: github.com/example/gen
plugins:
  - remote: buf.build/connectrpc/go:v1.18.0
    out: gen/go
    opt: paths=source_relative
  - remote: buf.build/protocolbuffers/go:v1.34.0
    out: gen/go
    opt: paths=source_relative
  - remote: buf.build/connectrpc/es:v2.0.0
    out: gen/ts
  - remote: buf.build/bufbuild/es:v2.0.0
    out: gen/ts
// Runtime: Go 1.22+, connect-go v1.18.0
// File: server/main.go
package main

import (
	"context"
	"fmt"
	"log"
	"net/http"
	"time"

	"connectrpc.com/connect"
	"connectrpc.com/grpcreflect"
	"github.com/example/gen/order/v1/orderv1connect"

	orderpb "github.com/example/gen/order/v1"
)

// OrderServiceHandler implements the order service
type OrderServiceHandler struct {
	orders map[string]*orderpb.Order
}

// CreateOrder creates a new order
func (h *OrderServiceHandler) CreateOrder(
	ctx context.Context,
	req *connect.Request[orderpb.CreateOrderRequest],
) (*connect.Response[orderpb.CreateOrderResponse], error) {
	msg := req.Msg

	orderID := fmt.Sprintf("ord-%d", time.Now().UnixNano())
	order := &orderpb.Order{
		OrderId:    orderID,
		UserId:     msg.UserId,
		Items:      msg.Items,
		Status:     "CREATED",
		CreatedAt:  time.Now().Unix(),
		UpdatedAt:  time.Now().Unix(),
	}
	h.orders[orderID] = order

	resp := connect.NewResponse(&orderpb.CreateOrderResponse{
		OrderId:   orderID,
		Status:    "CREATED",
		CreatedAt: order.CreatedAt,
	})

	resp.Header().Set("X-Request-Id", fmt.Sprintf("req-%d", time.Now().UnixNano()))
	return resp, nil
}

// GetOrder retrieves an order by ID
func (h *OrderServiceHandler) GetOrder(
	ctx context.Context,
	req *connect.Request[orderpb.GetOrderRequest],
) (*connect.Response[orderpb.GetOrderResponse], error) {
	order, exists := h.orders[req.Msg.OrderId]
	if !exists {
		return nil, connect.NewError(connect.CodeNotFound,
			fmt.Errorf("order %s not found", req.Msg.OrderId))
	}

	resp := connect.NewResponse(&orderpb.GetOrderResponse{
		Order: order,
	})
	return resp, nil
}

// StreamOrders streams orders to the client
func (h *OrderServiceHandler) StreamOrders(
	ctx context.Context,
	req *connect.Request[orderpb.StreamOrdersRequest],
	stream *connect.ServerStream[orderpb.Order],
) error {
	for _, order := range h.orders {
		if order.UserId == req.Msg.UserId {
			if err := stream.Send(order); err != nil {
				return fmt.Errorf("failed to stream order: %w", err)
			}
			time.Sleep(100 * time.Millisecond)
		}
	}
	return nil
}

// UploadOrders handles client streaming upload
func (h *OrderServiceHandler) UploadOrders(
	ctx context.Context,
	stream *connect.ClientStream[orderpb.UploadOrderRequest],
) (*connect.Response[orderpb.UploadOrdersResponse], error) {
	var totalCreated int32
	var orderIDs []string

	for stream.Receive() {
		msg := stream.Msg()
		orderID := fmt.Sprintf("ord-%d", time.Now().UnixNano())
		order := &orderpb.Order{
			OrderId:   orderID,
			UserId:    msg.UserId,
			Items:     msg.Items,
			Status:    "CREATED",
			CreatedAt: time.Now().Unix(),
		}
		h.orders[orderID] = order
		totalCreated++
		orderIDs = append(orderIDs, orderID)
	}

	if stream.Err() != nil {
		return nil, connect.NewError(connect.CodeInternal, stream.Err())
	}

	resp := connect.NewResponse(&orderpb.UploadOrdersResponse{
		TotalCreated: totalCreated,
		OrderIds:     orderIDs,
	})
	return resp, nil
}

// OrderChat handles bidirectional streaming chat
func (h *OrderServiceHandler) OrderChat(
	ctx context.Context,
	stream *connect.BidiStream[orderpb.ChatMessage, orderpb.ChatMessage],
) error {
	for {
		msg, err := stream.Receive()
		if err != nil {
			return nil
		}

		reply := &orderpb.ChatMessage{
			Sender:    "system",
			Message:   fmt.Sprintf("Received from %s: %s", msg.Sender, msg.Message),
			Timestamp: time.Now().Unix(),
		}

		if err := stream.Send(reply); err != nil {
			return fmt.Errorf("failed to send reply: %w", err)
		}
	}
}

func main() {
	handler := &OrderServiceHandler{
		orders: make(map[string]*orderpb.Order),
	}

	mux := http.NewServeMux()

	path, orderHandler := orderv1connect.NewOrderServiceHandler(handler)
	mux.Handle(path, orderHandler)

	reflector := grpcreflect.NewStaticReflector(
		orderv1connect.OrderServiceName,
	)
	mux.Handle(grpcreflect.NewHandlerV1(reflector))
	mux.Handle(grpcreflect.NewHandlerV1Alpha(reflector))

	log.Println("Connect-RPC server starting on :8080")
	log.Println("Supported protocols: gRPC, gRPC-Web, Connect")
	log.Fatal(http.ListenAndServe(":8080", mux))
}

Modèle 2 : Appels frontend gRPC-Web

Utilisez Connect-Web pour appeler les services gRPC directement depuis le frontend, éliminant la couche de middleware REST.

// Runtime: TypeScript 5.5+, @connectrpc/connect-web v2.0.0
// File: frontend/src/client.ts

import { createConnectTransport } from "@connectrpc/connect-web";
import { createClient } from "@connectrpc/connect";
import { OrderService } from "../gen/ts/order/v1/order_pb";

// Create Connect transport (HTTP/1.1 compatible)
const connectTransport = createConnectTransport({
  baseUrl: "https://api.example.com",
});

// Create gRPC-Web transport
const grpcWebTransport = createConnectTransport({
  baseUrl: "https://api.example.com",
  httpVersion: "2",
});

// Create order service client
const orderClient = createClient(OrderService, connectTransport);

// === Unary call ===
async function createOrder() {
  try {
    const response = await orderClient.createOrder({
      userId: "user-123",
      items: [
        { productId: "prod-1", productName: "Go Programming", quantity: 1, price: 89.0 },
        { productId: "prod-2", productName: "Rust in Action", quantity: 2, price: 99.0 },
      ],
      shippingAddress: "123 Main St, San Francisco",
    });

    console.log("Order created:", response.orderId, response.status);
  } catch (err) {
    console.error("Failed to create order:", err.code, err.message);
  }
}

// === Get order ===
async function getOrder(orderId: string) {
  try {
    const response = await orderClient.getOrder({ orderId });
    console.log("Order details:", response.order);
  } catch (err: any) {
    if (err.code === "NOT_FOUND") {
      console.warn("Order not found");
    } else {
      console.error("Failed to get order:", err.message);
    }
  }
}

// === Server Streaming ===
async function streamOrders(userId: string) {
  try {
    for await (const order of orderClient.streamOrders({ userId })) {
      console.log("Real-time order update:", order.orderId, order.status);
      updateOrderInUI(order);
    }
  } catch (err) {
    console.error("Order stream interrupted:", err.message);
  }
}

// === Client Streaming ===
async function uploadOrders() {
  const orders = [
    { userId: "user-1", items: [{ productId: "p1", productName: "Item 1", quantity: 1, price: 10 }] },
    { userId: "user-2", items: [{ productId: "p2", productName: "Item 2", quantity: 2, price: 20 }] },
  ];

  try {
    const response = await orderClient.uploadOrders(orders);
    console.log(`Successfully uploaded ${response.totalCreated} orders`);
  } catch (err) {
    console.error("Failed to upload orders:", err.message);
  }
}

function useOrderService() {
  return { createOrder, getOrder, streamOrders, uploadOrders };
}

function updateOrderInUI(order: any) {}

Modèle 3 : Gestion des erreurs et nouvelles tentatives

Connect-RPC fournit un mécanisme unifié de gestion des erreurs — le frontend et le backend utilisent les mêmes codes d'erreur et le même format de message.

// Runtime: Go 1.22+, connect-go v1.18.0
// File: server/errors.go
package main

import (
	"context"
	"fmt"
	"log"
	"net/http"
	"time"

	"connectrpc.com/connect"
)

// OrderError represents a business error
type OrderError struct {
	Code      connect.Code `json:"code"`
	Message   string       `json:"message"`
	Detail    string       `json:"detail,omitempty"`
	Retryable bool         `json:"retryable"`
}

func (e *OrderError) Error() string {
	return fmt.Sprintf("[%s] %s: %s", e.Code, e.Message, e.Detail)
}

// ToConnectError converts to a Connect error
func (e *OrderError) ToConnectError() *connect.Error {
	err := connect.NewError(e.Code, fmt.Errorf("%s: %s", e.Message, e.Detail))
	if e.Retryable {
		err.Meta().Set("Retry-After", "5")
		err.Meta().Set("X-Retryable", "true")
	}
	return err
}

var (
	ErrOrderNotFound = &OrderError{
		Code:      connect.CodeNotFound,
		Message:   "Order not found",
		Retryable: false,
	}
	ErrOrderAlreadyCancelled = &OrderError{
		Code:      connect.CodeFailedPrecondition,
		Message:   "Order already cancelled",
		Retryable: false,
	}
	ErrInsufficientStock = &OrderError{
		Code:      connect.CodeResourceExhausted,
		Message:   "Insufficient stock",
		Retryable: true,
	}
	ErrPaymentTimeout = &OrderError{
		Code:      connect.CodeDeadlineExceeded,
		Message:   "Payment timeout",
		Retryable: true,
	}
)

// RetryInterceptor implements client-side retry logic
type RetryInterceptor struct {
	maxRetries     int
	initialDelay   time.Duration
	maxDelay       time.Duration
	retryableCodes map[connect.Code]bool
}

func NewRetryInterceptor() *RetryInterceptor {
	return &RetryInterceptor{
		maxRetries:   3,
		initialDelay: 100 * time.Millisecond,
		maxDelay:     5 * time.Second,
		retryableCodes: map[connect.Code]bool{
			connect.CodeUnavailable:       true,
			connect.CodeResourceExhausted: true,
			connect.CodeDeadlineExceeded:  true,
			connect.CodeAborted:           true,
		},
	}
}

func (i *RetryInterceptor) WrapUnary(next connect.UnaryFunc) connect.UnaryFunc {
	return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) {
		var lastErr error

		for attempt := 0; attempt <= i.maxRetries; attempt++ {
			resp, err := next(ctx, req)
			if err == nil {
				return resp, nil
			}

			connectErr, ok := err.(*connect.Error)
			if !ok {
				return nil, err
			}

			if !i.retryableCodes[connectErr.Code()] {
				return nil, err
			}

			retryAfter := connectErr.Meta().Get("Retry-After")
			delay := i.calculateDelay(attempt, retryAfter)

			lastErr = err
			log.Printf("Retry %d/%d, delay %v, error: %v",
				attempt+1, i.maxRetries, delay, connectErr.Message())

			select {
			case <-time.After(delay):
			case <-ctx.Done():
				return nil, ctx.Err()
			}
		}

		return nil, lastErr
	}
}

func (i *RetryInterceptor) calculateDelay(attempt int, retryAfter string) time.Duration {
	if retryAfter != "" {
		if d, err := time.ParseDuration(retryAfter + "s"); err == nil {
			return d
		}
	}

	delay := i.initialDelay * time.Duration(1<<uint(attempt))
	if delay > i.maxDelay {
		delay = i.maxDelay
	}
	return delay
}

// RecoveryInterceptor recovers from panics in handlers
func RecoveryInterceptor() connect.UnaryInterceptorFunc {
	return func(next connect.UnaryFunc) connect.UnaryFunc {
		return func(ctx context.Context, req connect.AnyRequest) (resp connect.AnyResponse, err error) {
			defer func() {
				if r := recover(); r != nil {
					log.Printf("[PANIC] Handler panic: %v", r)
					err = connect.NewError(connect.CodeInternal,
						fmt.Errorf("internal server error, please retry later"))
				}
			}()
			return next(ctx, req)
		}
	}
}

// ErrorLoggingInterceptor logs errors
func ErrorLoggingInterceptor() connect.UnaryInterceptorFunc {
	return func(next connect.UnaryFunc) connect.UnaryFunc {
		return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) {
			startTime := time.Now()
			resp, err := next(ctx, req)

			if err != nil {
				connectErr, ok := err.(*connect.Error)
				if ok {
					log.Printf("[ERROR] method=%s code=%s msg=%s duration=%v",
						req.Spec().Procedure,
						connectErr.Code(),
						connectErr.Message(),
						time.Since(startTime),
					)
				}
			}

			return resp, err
		}
	}
}

func main() {
	mux := http.NewServeMux()

	interceptors := []connect.Interceptor{
		RecoveryInterceptor(),
		ErrorLoggingInterceptor(),
	}

	_ = interceptors

	log.Println("Error handling service starting on :8080")
	log.Fatal(http.ListenAndServe(":8080", mux))
}

Modèle 4 : Communication en streaming

Connect-RPC prend entièrement en charge trois modes de communication en streaming avec des définitions Protobuf unifiées pour le frontend et le backend.

// Runtime: Go 1.22+, connect-go v1.18.0
// File: server/streaming.go
package main

import (
	"context"
	"fmt"
	"io"
	"log"
	"math/rand"
	"net/http"
	"sync"
	"time"

	"connectrpc.com/connect"
)

// OrderStatusStream manages order status subscriptions
type OrderStatusStream struct {
	subscribers map[string]chan *OrderStatusUpdate
	mu          sync.RWMutex
}

type OrderStatusUpdate struct {
	OrderID string `json:"order_id"`
	Status  string `json:"status"`
	Message string `json:"message"`
}

func NewOrderStatusStream() *OrderStatusStream {
	return &OrderStatusStream{
		subscribers: make(map[string]chan *OrderStatusUpdate),
	}
}

func (s *OrderStatusStream) Subscribe(userID string) <-chan *OrderStatusUpdate {
	s.mu.Lock()
	defer s.mu.Unlock()

	ch := make(chan *OrderStatusUpdate, 100)
	s.subscribers[userID] = ch
	return ch
}

func (s *OrderStatusStream) Unsubscribe(userID string) {
	s.mu.Lock()
	defer s.mu.Unlock()

	if ch, ok := s.subscribers[userID]; ok {
		close(ch)
		delete(s.subscribers, userID)
	}
}

func (s *OrderStatusStream) Publish(update *OrderStatusUpdate) {
	s.mu.RLock()
	defer s.mu.RUnlock()

	for _, ch := range s.subscribers {
		select {
		case ch <- update:
		default:
			log.Printf("Subscriber channel full, dropping update: %s", update.OrderID)
		}
	}
}

// BatchOrderImporter handles batch order imports
type BatchOrderImporter struct {
	processedCount int
	failedCount    int
	mu             sync.Mutex
}

type OrderImportRequest struct {
	UserID string `json:"user_id"`
	Data   string `json:"data"`
}

type BatchImportResult struct {
	BatchID        string `json:"batch_id"`
	ProcessedCount int32  `json:"processed_count"`
	FailedCount    int32  `json:"failed_count"`
}

func (b *BatchOrderImporter) ProcessStream(
	ctx context.Context,
	stream *connect.ClientStream[OrderImportRequest],
) (*BatchImportResult, error) {
	batchID := fmt.Sprintf("batch-%d", time.Now().UnixNano())

	for stream.Receive() {
		req := stream.Msg()
		if err := b.processOne(ctx, req); err != nil {
			b.mu.Lock()
			b.failedCount++
			b.mu.Unlock()
			continue
		}
		b.mu.Lock()
		b.processedCount++
		b.mu.Unlock()
	}

	if stream.Err() != nil {
		return nil, connect.NewError(connect.CodeInternal, stream.Err())
	}

	return &BatchImportResult{
		BatchID:        batchID,
		ProcessedCount: int32(b.processedCount),
		FailedCount:    int32(b.failedCount),
	}, nil
}

func (b *BatchOrderImporter) processOne(ctx context.Context, req *OrderImportRequest) error {
	select {
	case <-time.After(time.Duration(rand.Intn(50)) * time.Millisecond):
	case <-ctx.Done():
		return ctx.Err()
	}
	return nil
}

// CollaborationRoom manages real-time collaboration
type CollaborationRoom struct {
	clients map[string]chan *CollabMessage
	mu      sync.RWMutex
}

type CollabMessage struct {
	UserID  string `json:"user_id"`
	Content string `json:"content"`
	Type    string `json:"type"`
}

func (r *CollaborationRoom) Join(userID string) chan *CollabMessage {
	r.mu.Lock()
	defer r.mu.Unlock()

	ch := make(chan *CollabMessage, 50)
	r.clients[userID] = ch
	return ch
}

func (r *CollaborationRoom) Leave(userID string) {
	r.mu.Lock()
	defer r.mu.Unlock()

	if ch, ok := r.clients[userID]; ok {
		close(ch)
		delete(r.clients, userID)
	}
}

func (r *CollaborationRoom) Broadcast(msg *CollabMessage, excludeUserID string) {
	r.mu.RLock()
	defer r.mu.RUnlock()

	for userID, ch := range r.clients {
		if userID == excludeUserID {
			continue
		}
		select {
		case ch <- msg:
		default:
			log.Printf("Client %s channel full", userID)
		}
	}
}

func (r *CollaborationRoom) HandleCollabStream(
	ctx context.Context,
	stream *connect.BidiStream[CollabMessage, CollabMessage],
) error {
	userID := stream.RequestHeader().Get("X-User-Id")
	if userID == "" {
		userID = fmt.Sprintf("user-%d", rand.Int63())
	}

	recvCh := r.Join(userID)
	defer r.Leave(userID)

	errCh := make(chan error, 1)
	go func() {
		for {
			msg, err := stream.Receive()
			if err != nil {
				if err == io.EOF {
					errCh <- nil
					return
				}
				errCh <- err
				return
			}
			r.Broadcast(msg, userID)
		}
	}()

	for {
		select {
		case msg := <-recvCh:
			if err := stream.Send(msg); err != nil {
				return fmt.Errorf("failed to send collab message: %w", err)
			}
		case err := <-errCh:
			return err
		case <-ctx.Done():
			return ctx.Err()
		}
	}
}

func main() {
	room := &CollaborationRoom{
		clients: make(map[string]chan *CollabMessage),
	}
	statusStream := NewOrderStatusStream()

	_ = room
	_ = statusStream

	log.Println("Streaming service starting on :8080")
	log.Fatal(http.ListenAndServe(":8080", nil))
}

Modèle 5 : Passerelle Connect prête pour la production

Construisez une passerelle Connect-RPC prête pour la production avec authentification, limitation de débit, surveillance et arrêt gracieux.

// Runtime: Go 1.22+, connect-go v1.18.0
// File: server/gateway.go
package main

import (
	"context"
	"fmt"
	"log"
	"net/http"
	"os"
	"os/signal"
	"strings"
	"sync/atomic"
	"syscall"
	"time"

	"connectrpc.com/connect"
	"connectrpc.com/grpcreflect"
	"golang.org/x/net/http2"
	"golang.org/x/net/http2/h2c"
)

func AuthInterceptor(jwtSecret string) connect.UnaryInterceptorFunc {
	return func(next connect.UnaryFunc) connect.UnaryFunc {
		return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) {
			if strings.HasSuffix(req.Spec().Procedure, "/Health/Check") {
				return next(ctx, req)
			}

			token := req.Header().Get("Authorization")
			if token == "" {
				return nil, connect.NewError(connect.CodeUnauthenticated,
					fmt.Errorf("missing authentication token"))
			}

			token = strings.TrimPrefix(token, "Bearer ")
			claims, err := validateJWT(token, jwtSecret)
			if err != nil {
				return nil, connect.NewError(connect.CodeUnauthenticated,
					fmt.Errorf("invalid authentication token: %w", err))
			}

			ctx = context.WithValue(ctx, "userID", claims.UserID)
			ctx = context.WithValue(ctx, "scopes", claims.Scopes)

			return next(ctx, req)
		}
	}
}

type JWTClaims struct {
	UserID string   `json:"user_id"`
	Scopes []string `json:"scopes"`
}

func validateJWT(token, secret string) (*JWTClaims, error) {
	return &JWTClaims{
		UserID: "user-from-token",
		Scopes: []string{"orders:read", "orders:write"},
	}, nil
}

type RateLimiter struct {
	tokens     atomic.Int64
	maxTokens  int64
	refillRate time.Duration
}

func NewRateLimiter(maxTokens int64, refillRate time.Duration) *RateLimiter {
	rl := &RateLimiter{
		maxTokens:  maxTokens,
		refillRate: refillRate,
	}
	rl.tokens.Store(maxTokens)

	go func() {
		ticker := time.NewTicker(refillRate)
		defer ticker.Stop()
		for range ticker.C {
			current := rl.tokens.Load()
			if current < maxTokens {
				rl.tokens.CompareAndSwap(current, current+1)
			}
		}
	}()

	return rl
}

func (rl *RateLimiter) Allow() bool {
	for {
		current := rl.tokens.Load()
		if current <= 0 {
			return false
		}
		if rl.tokens.CompareAndSwap(current, current-1) {
			return true
		}
	}
}

func RateLimitInterceptor(limiter *RateLimiter) connect.UnaryInterceptorFunc {
	return func(next connect.UnaryFunc) connect.UnaryFunc {
		return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) {
			if !limiter.Allow() {
				return nil, connect.NewError(connect.CodeResourceExhausted,
					fmt.Errorf("rate limit exceeded, please retry later"))
			}
			return next(ctx, req)
		}
	}
}

func TracingInterceptor() connect.UnaryInterceptorFunc {
	return func(next connect.UnaryFunc) connect.UnaryFunc {
		return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) {
			startTime := time.Now()
			traceID := req.Header().Get("X-Trace-Id")
			if traceID == "" {
				traceID = fmt.Sprintf("trace-%d", startTime.UnixNano())
			}

			log.Printf("[TRACE] id=%s method=%s start=%v",
				traceID, req.Spec().Procedure, startTime)

			resp, err := next(ctx, req)

			duration := time.Since(startTime)
			status := "OK"
			if err != nil {
				status = "ERROR"
			}

			log.Printf("[TRACE] id=%s method=%s status=%s duration=%v",
				traceID, req.Spec().Procedure, status, duration)

			if resp != nil {
				resp.Header().Set("X-Trace-Id", traceID)
				resp.Header().Set("X-Response-Time", duration.String())
			}

			return resp, err
		}
	}
}

type ConnectGateway struct {
	server     *http.Server
	limiter    *RateLimiter
	shutdownCh chan os.Signal
}

func NewConnectGateway(addr string) *ConnectGateway {
	gw := &ConnectGateway{
		limiter:    NewRateLimiter(1000, time.Second),
		shutdownCh: make(chan os.Signal, 1),
	}

	mux := http.NewServeMux()

	reflector := grpcreflect.NewStaticReflector()
	mux.Handle(grpcreflect.NewHandlerV1(reflector))

	mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
		w.Write([]byte("ok"))
	})

	interceptors := []connect.Interceptor{
		TracingInterceptor(),
		RateLimitInterceptor(gw.limiter),
		AuthInterceptor("your-jwt-secret"),
	}

	_ = interceptors

	gw.server = &http.Server{
		Addr:    addr,
		Handler: h2c.NewHandler(mux, &http2.Server{}),
	}

	return gw
}

func (gw *ConnectGateway) Start() error {
	signal.Notify(gw.shutdownCh, syscall.SIGINT, syscall.SIGTERM)

	errCh := make(chan error, 1)
	go func() {
		log.Printf("Connect gateway starting on %s", gw.server.Addr)
		log.Println("Supported protocols: gRPC, gRPC-Web, Connect (HTTP/1.1 & HTTP/2)")
		if err := gw.server.ListenAndServe(); err != http.ErrServerClosed {
			errCh <- err
		}
	}()

	select {
	case err := <-errCh:
		return fmt.Errorf("gateway failed to start: %w", err)
	case sig := <-gw.shutdownCh:
		log.Printf("Received signal %v, starting graceful shutdown...", sig)
		return gw.Shutdown()
	}
}

func (gw *ConnectGateway) Shutdown() error {
	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	log.Println("Stopping new request acceptance...")
	if err := gw.server.Shutdown(ctx); err != nil {
		return fmt.Errorf("gateway shutdown failed: %w", err)
	}

	log.Println("Gateway gracefully shut down")
	return nil
}

func main() {
	gateway := NewConnectGateway(":8080")
	if err := gateway.Start(); err != nil {
		log.Fatalf("Gateway failed: %v", err)
	}
}

Guide des pièges : 5 pièges de niveau production

Piège 1 : Conflits de versions de génération de code Buf. Différentes versions de plugins génèrent du code incompatible, provoquant des échecs de compilation. Solution : Verrouiller les versions de plugins dans buf.gen.yaml, utiliser buf generate en CI au lieu de protoc local.

Piège 2 : Échec de la négociation HTTP/2. Certains proxies inverses (anciennes versions de Nginx) ne prennent pas en charge HTTP/2, provoquant des échecs d'appels gRPC. Solution : Utiliser le protocole Connect (compatible HTTP/1.1) comme solution de secours, ou mettre à jour les proxies pour prendre en charge h2c.

Piège 3 : Déconnexion de stream passée inaperçue. La gigue réseau provoque la coupure des connexions de streaming sans que le serveur le sache. Solution : Implémenter des mécanismes de heartbeat (ping toutes les 30 secondes), définir des timeouts de lecture/écriture et la reconnexion automatique du client.

Piège 4 : Les corps de messages volumineux provoquent des OOM. Le streaming de téléchargements de fichiers volumineux remplit le tampon du récepteur, provoquant des OOM. Solution : Définir une taille maximale du corps du message (la valeur par défaut de Connect est de 4 Mo), traiter en chunks de streaming, implémenter la contre-pression.

Piège 5 : Configuration CORS manquante. Les appels Connect côté navigateur sont bloqués par la politique CORS. Solution : Configurer le middleware CORS au niveau de la passerelle, autorisant Content-Type: application/proto et application/json.

Référence rapide de dépannage des erreurs

Message d'erreur Cause Solution
connect: code = Unauthenticated Jeton d'authentification manquant ou invalide Vérifier l'en-tête Authorization et la validité du JWT
connect: code = NotFound La ressource demandée n'existe pas Vérifier les paramètres de requête et l'ID de ressource
connect: code = ResourceExhausted Limite de débit de requêtes dépassée Réduire la fréquence des requêtes ou ajuster la configuration du limiteur
connect: code = Unavailable Service indisponible Vérifier la santé du service et la connectivité réseau
http2: frame too large Le corps du message dépasse la limite par défaut Ajuster l'option connect.MaxRecvMsgSize
CORS policy: No Access-Control-Allow-Origin Configuration CORS manquante Ajouter le middleware CORS au niveau de la passerelle
proto: invalid wire format Inadéquation d'encodage/décodage Protobuf Vérifier la cohérence de version du fichier Proto
buf: plugin not found Plugin Buf non installé Exécuter buf generate avec des plugins distants
stream recv: context canceled Le client a annulé la requête ou dépassé le délai Augmenter le timeout ou vérifier la logique client
tls: handshake failure Mauvaise configuration TLS Vérifier la configuration des certificats ou utiliser h2c pour le développement

Optimisation avancée : 5 conseils de niveau production

Conseil 1 : Gestion des versions de fichiers Proto. Utiliser le BSR (Buf Schema Registry) de Buf pour gérer les versions de fichiers Proto, permettant le versionnage d'API et les vérifications de compatibilité ascendante.

Conseil 2 : Stratégie de repli du protocole Connect. Sélection automatique du protocole selon la capacité du client : gRPC (backend-à-backend), gRPC-Web (anciens navigateurs), Connect (navigateurs modernes) — sans changement manuel nécessaire.

Conseil 3 : Contrôle de la contre-pression en streaming. Implémenter la contre-pression dans le streaming serveur en ajustant dynamiquement le taux de push selon la vitesse de consommation du client, empêchant le débordement du tampon.

Conseil 4 : Regroupement de requêtes. Utiliser le Client Streaming de Connect pour fusionner plusieurs petites requêtes en une seule requête de streaming, réduisant les allers-retours réseau.

Conseil 5 : Intégration de l'observabilité. Intégrer OpenTelemetry dans les intercepteurs pour générer automatiquement des Spans et des Métriques, incorporant les chaînes d'appels Connect dans le traçage distribué complet.

Analyse comparative

Dimension REST + JSON gRPC gRPC-Connect
Protocole HTTP/1.1 HTTP/2 HTTP/1.1 + HTTP/2
Format de données JSON Protobuf Protobuf + JSON
Support navigateur Natif Nécessite gRPC-Web Natif (protocole Connect)
Streaming WebSocket Natif Natif
Génération de code OpenAPI/Swagger protoc Buf
Typage sûr Faible (JSON) Fort (Protobuf) Fort (Protobuf)
Gestion des erreurs Code d'état HTTP Statut gRPC Statut Connect (compatible gRPC)
Courbe d'apprentissage Faible Moyenne Moyenne
Performance Moyenne Élevée Élevée
Unification frontend-backend Non Non Oui

Conclusion

Les 5 modèles essentiels du protocole gRPC-Connect résolvent les points de douleur clés de la communication frontend-backend des microservices : la définition de services Connect-RPC permet un Proto pour les deux côtés, les appels frontend gRPC-Web éliminent la couche de middleware REST, la gestion unifiée des erreurs et les mécanismes de nouvelle tentative assurent la fiabilité, la communication en streaming fournit des capacités complètes de communication en temps réel et la passerelle Connect prête pour la production assure la stabilité du système.

gRPC-Connect n'est pas un remplacement de REST — c'est la meilleure pratique pour la communication frontend-backend unifiée. Si vous construisez un système de microservices Go, l'écosystème gRPC-Connect + Buf est la stack technologique dans laquelle il vaut la peine d'investir en 2026. Rappelez-vous : une définition, les deux côtés fonctionnent — c'est la valeur essentielle de gRPC-Connect.

Outils en ligne recommandés

  • /en/json/format — Formateur JSON pour visualiser les requêtes et réponses JSON du protocole Connect
  • /en/dev/curl-to-code — Convertisseur cURL vers code pour générer rapidement du code d'appel client Connect
  • /en/encode/hash — Calculateur de hash pour vérifier les résumés de messages Protobuf
  • /en/text/diff — Outil de diff de texte pour comparer les modifications de fichiers Proto entre les versions

Essayez ces outils exécutés localement dans le navigateur — aucune inscription requise →

#gRPC-Connect#Go微服务#前后端通信#Connect-RPC#Buf#2026#技术架构