gRPC-GoMiddleTechnical

Как работают client/server interceptors и где реализовать auth, logging и retry?

Interceptors — это middleware для gRPC в Go. Unary и stream interceptors перехватывают вызовы на клиенте и сервере; через них реализуются auth, logging, retry и трейсинг, объединяемые цепочкой через grpc.ChainUnaryInterceptor.

Что такое gRPC interceptors

Interceptors в gRPC — аналог middleware в HTTP-фреймворках. Они оборачивают обработку RPC-вызова на стороне сервера или клиента. Существует два вида: UnaryInterceptor (для обычных запросов) и StreamInterceptor (для стриминга).

Серверные interceptors

package main

import (
	"context"
	"log"
	"time"

	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/metadata"
	"google.golang.org/grpc/status"
)

// Auth interceptor — проверяет Bearer-токен в метаданных
func authInterceptor(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
	md, ok := metadata.FromIncomingContext(ctx)
	if !ok {
		return nil, status.Error(codes.Unauthenticated, "missing metadata")
	}
	tokens := md.Get("authorization")
	if len(tokens) == 0 || !validateToken(tokens[0]) {
		return nil, status.Error(codes.Unauthenticated, "invalid token")
	}
	return handler(ctx, req)
}

// Logging interceptor — логирует метод, длительность и код ответа
func loggingInterceptor(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
	start := time.Now()
	resp, err := handler(ctx, req)
	st, _ := status.FromError(err)
	log.Printf("method=%s duration=%s code=%s", info.FullMethod, time.Since(start), st.Code())
	return resp, err
}

// Запуск сервера с цепочкой interceptors
func main() {
	s := grpc.NewServer(
		grpc.ChainUnaryInterceptor(
			loggingInterceptor, // выполняется первым
			authInterceptor,    // выполняется вторым
		),
	)
	// регистрация сервисов...
	_ = s
}

func validateToken(token string) bool {
	return token == "Bearer secret" // упрощённо
}

Клиентские interceptors — retry

// Retry interceptor — повторяет запрос при временных ошибках
func retryInterceptor(maxAttempts int) grpc.UnaryClientInterceptor {
	return func(ctx context.Context, method string, req, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
		var err error
		for i := range maxAttempts {
			err = invoker(ctx, method, req, reply, cc, opts...)
			if err == nil {
				return nil
			}
			st, _ := status.FromError(err)
			if st.Code() != codes.Unavailable && st.Code() != codes.DeadlineExceeded {
				return err // не ретраить non-retryable ошибки
			}
			backoff := time.Duration(i+1) * 100 * time.Millisecond
			log.Printf("retry %d/%d after %s", i+1, maxAttempts, backoff)
			time.Sleep(backoff)
		}
		return err
	}
}

// Клиент с interceptors
func newClient() (*grpc.ClientConn, error) {
	return grpc.NewClient(
		"localhost:50051",
		grpc.WithChainUnaryInterceptor(
			retryInterceptor(3),
		),
	)
}

Stream interceptors

// Серверный stream interceptor для логирования
func streamLoggingInterceptor(srv any, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
	log.Printf("stream started: method=%s isClientStream=%v isServerStream=%v",
		info.FullMethod, info.IsClientStream, info.IsServerStream)
	err := handler(srv, ss)
	log.Printf("stream ended: method=%s err=%v", info.FullMethod, err)
	return err
}

// Регистрация stream interceptor
func newServerWithStream() *grpc.Server {
	return grpc.NewServer(
		grpc.ChainStreamInterceptor(streamLoggingInterceptor),
	)
}

Передача данных через контекст

Interceptors часто добавляют данные в контекст для следующих обработчиков. Используйте typed keys, чтобы избежать коллизий:

type contextKey string

const userIDKey contextKey = "userID"

func authInterceptorWithCtx(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
	userID := extractUserID(ctx)
	ctx = context.WithValue(ctx, userIDKey, userID)
	return handler(ctx, req)
}

func extractUserID(ctx context.Context) string {
	return "user-123" // из JWT или metadata
}

Подводные камни

  • Порядок в цепочке важен — logging должен быть внешним (первым), auth — внутренним. Иначе неаутентифицированные запросы не логируются или auth выполняется после бизнес-логики.
  • Паника в interceptor — если interceptor не перехватывает panic, сервер падает полностью. Нужен recovery interceptor как самый внешний в цепочке.
  • grpc.UnaryInterceptor vs grpc.ChainUnaryInterceptor — старый API grpc.UnaryInterceptor принимает только один interceptor. При передаче нескольких — только последний действует. Всегда используйте ChainUnaryInterceptor.
  • Context cancellation — retry-interceptor не проверяет ctx.Err() перед следующей попыткой, что ведёт к лишним запросам после таймаута клиента.
  • Stream interceptors сложнее unary — для добавления логики в каждое сообщение стрима нужно оборачивать grpc.ServerStream кастомной структурой, переопределяя RecvMsg/SendMsg.
  • Middleware от сторонних библиотек (go-grpc-middleware) — не все совместимы с актуальным grpc-go API (v1.64+), проверяйте совместимость версий.
  • Блокирующий retry без backoff — простой retry без экспоненциального отступа создаёт thundering herd при массовых отказах.

Common mistakes

  • Отвечать определением без production-сценария.
  • Не называть runtime boundary, security boundary или failure mode.
  • Игнорировать версию API, observability и тестовую проверку.

What the interviewer is testing

  • Объясняет механизм своими словами и без выдуманных API.
  • Называет реальные риски, диагностику и критерий корректности.
  • Связывает ответ с текущей документацией и миграционными ограничениями.

Sources

Related topics