gRPC-GoSeniorSystem design

Как сопоставить gRPC status codes с HTTP status codes при использовании gateway?

gRPC статусы маппируются в HTTP по стандартной таблице: NOT_FOUND=404, UNAUTHENTICATED=401, PERMISSION_DENIED=403, UNAVAILABLE=503, RESOURCE_EXHAUSTED=429. grpc-gateway применяет маппинг автоматически через runtime.HTTPStatusFromCode.

Маппинг gRPC Status Codes в HTTP Status Codes

При использовании gRPC-Gateway или любого другого HTTP-to-gRPC транслятора необходимо корректно преобразовывать gRPC-коды ошибок в HTTP-статусы. Google определил официальную таблицу соответствия, которую реализует пакет google.golang.org/grpc/codes.

Официальная таблица маппинга

  • OK (0) → 200 OK
  • CANCELLED (1) → 499 Client Closed Request
  • UNKNOWN (2) → 500 Internal Server Error
  • INVALID_ARGUMENT (3) → 400 Bad Request
  • DEADLINE_EXCEEDED (4) → 504 Gateway Timeout
  • NOT_FOUND (5) → 404 Not Found
  • ALREADY_EXISTS (6) → 409 Conflict
  • PERMISSION_DENIED (7) → 403 Forbidden
  • RESOURCE_EXHAUSTED (8) → 429 Too Many Requests
  • FAILED_PRECONDITION (9) → 400 Bad Request
  • ABORTED (10) → 409 Conflict
  • OUT_OF_RANGE (11) → 400 Bad Request
  • UNIMPLEMENTED (12) → 501 Not Implemented
  • INTERNAL (13) → 500 Internal Server Error
  • UNAVAILABLE (14) → 503 Service Unavailable
  • DATA_LOSS (15) → 500 Internal Server Error
  • UNAUTHENTICATED (16) → 401 Unauthorized

Реализация через grpc-gateway

grpc-gateway автоматически применяет этот маппинг, но его можно переопределить через кастомный ServeMuxOption:

import (
	"net/http"
	"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

func customErrorHandler(
	ctx context.Context,
	mux *runtime.ServeMux,
	marshaler runtime.Marshaler,
	w http.ResponseWriter,
	r *http.Request,
	err error,
) {
	st := status.Convert(err)

	httpCode := runtime.HTTPStatusFromCode(st.Code())

	// Кастомизация: FAILED_PRECONDITION -> 422 Unprocessable Entity
	if st.Code() == codes.FailedPrecondition {
		httpCode = http.StatusUnprocessableEntity
	}

	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(httpCode)

	body, _ := marshaler.Marshal(map[string]interface{}{
		"code":    st.Code().String(),
		"message": st.Message(),
		"details": st.Details(),
	})
	w.Write(body)
}

mux := runtime.NewServeMux(
	runtime.WithErrorHandler(customErrorHandler),
)

Использование runtime.HTTPStatusFromCode

import "github.com/grpc-ecosystem/grpc-gateway/v2/runtime"

// Получение HTTP-кода из gRPC-кода
httpStatus := runtime.HTTPStatusFromCode(codes.NotFound) // 404
httpStatus = runtime.HTTPStatusFromCode(codes.Unauthenticated) // 401
httpStatus = runtime.HTTPStatusFromCode(codes.ResourceExhausted) // 429

Передача деталей ошибки через status.Details

import (
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"google.golang.org/genproto/googleapis/rpc/errdetails"
)

func (s *server) CreateUser(ctx context.Context, req *pb.CreateUserRequest) (*pb.User, error) {
	st := status.New(codes.AlreadyExists, "user already exists")
	st, _ = st.WithDetails(&errdetails.ErrorInfo{
		Reason: "USER_ALREADY_EXISTS",
		Domain: "myservice.example.com",
	})
	return nil, st.Err()
}
// HTTP: 409 Conflict + JSON с details

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

  • CANCELLED маппируется в 499 — нестандартный код nginx; некоторые клиенты его не понимают. Рассмотрите переопределение на 400 или 503.
  • FAILED_PRECONDITION и OUT_OF_RANGE оба маппируются в 400 — это семантически разные ошибки; часто нужно переопределять на 422.
  • Не используйте INTERNAL для валидационных ошибок — клиент получит 500 и не поймёт, что запрос неверный.
  • grpc-gateway по умолчанию не включает details в JSON-ответ без кастомного marshaler — добавьте явно.
  • HTTP/1.1 клиенты не получают gRPC-трейлеры с кодами ошибок — весь маппинг должен идти через HTTP status code.
  • Код UNAUTHENTICATED → 401, а не 403 (403 = PERMISSION_DENIED) — частая путаница при проектировании API.
  • При использовании Envoy как gateway маппинг может отличаться от grpc-gateway — сверяйтесь с документацией конкретного proxy.
  • DEADLINE_EXCEEDED → 504, но если timeout истёк на стороне клиента до получения ответа — клиент видит network error, а не 504.

Common mistakes

  • Давать ответ про сопоставление gRPC и HTTP status codes только на уровне определения, не показывая поведение в реальном приложении.
  • Игнорировать границы ответственности вокруг темы «сопоставление gRPC и HTTP status codes»: кто отменяет работу, кто владеет ресурсом и где формируется ответ клиенту.
  • Не связывать сопоставление gRPC и HTTP status codes с observability, тестированием или безопасностью, когда это влияет на продакшен-поведение.

What the interviewer is testing

  • Точно объясняет, что именно делает сопоставление gRPC и HTTP status codes и где это используется в Go-коде.
  • Связывает сопоставление gRPC и HTTP status codes с корректным lifecycle запроса, отменой, конкурентностью или конфигурацией сервера там, где это уместно.
  • Не изобретает API и опирается на реальные контракты официальной документации.

Sources

Related topics