Testland
Browse all skills & agents

grpc-status-code-mapping-reference

Pure-reference catalog of gRPC standard status codes - the 17 canonical codes (OK..UNAUTHENTICATED), their numeric values, semantics, retry behaviour per AIP-194 (only UNAVAILABLE is auto-retry-safe), and the gRPC-to-HTTP status mapping used by grpc-gateway (NOT_FOUND→404, INVALID_ARGUMENT→400, PERMISSION_DENIED→403, UNAUTHENTICATED→401, RESOURCE_EXHAUSTED→429, FAILED_PRECONDITION→400 not 412, ABORTED→409, UNAVAILABLE→503, DEADLINE_EXCEEDED→504, etc.). Use when designing a gRPC service's error vocabulary, writing assertions in gRPC client tests, configuring retry policies, or mapping gRPC errors to HTTP via a gateway. Consumed by buf-cli-lint-breaking-build, ghz-load, grpcurl-cli, grpc-mock, grpc-streaming-test-author.

Install with skills.sh (any agent)

npx skills add testland/qa --skill grpc-status-code-mapping-reference
View source

grpc-status-code-mapping-reference

Overview

gRPC defines 17 standard status codes (per grpc.io/docs/guides/status-codes/ (opens in new window)) that every implementation respects. They are the wire-level error vocabulary; using the wrong code breaks retry behaviour, HTTP gateway translation, and observability dashboards.

Three things to get right:

  1. The semantic meaning - OK..UNAUTHENTICATED have specific definitions; picking the wrong one mis-signals to clients.
  2. The retry behaviour - per AIP-194 (opens in new window), only UNAVAILABLE is generally safe to auto-retry.
  3. The HTTP mapping - per grpc-gateway runtime/errors.go (opens in new window), FAILED_PRECONDITION → 400 Bad Request (NOT 412 "Precondition Failed" despite the name).

This skill is a pure reference consumed by client-test authors, server implementers, and the gRPC-to-HTTP gateway configurators.

When to use

  • Designing the error vocabulary of a new gRPC service.
  • Writing client-test assertions: which status should the test expect?
  • Configuring a retry policy in the gRPC client.
  • Mapping gRPC errors to HTTP in a grpc-gateway setup.
  • PR review - is this status.Errorf call using the right code?

The canonical 17 codes

Per grpc.io/docs/guides/status-codes/ (opens in new window):

#CodeDefinition (gRPC docs)Typical example
0OK"Not an error; returned on success."Successful RPC
1CANCELLED"The operation was cancelled, typically by the caller."Client terminates stream
2UNKNOWN"Unknown error... errors raised by APIs that do not return enough error information."Wrapping a non-gRPC error
3INVALID_ARGUMENT"The client specified an invalid argument... problematic regardless of the state of the system."Malformed request payload
4DEADLINE_EXCEEDED"The deadline expired before the operation could complete... even if the operation has completed successfully."Server slow + deadline expired
5NOT_FOUND"Some requested entity (e.g., file or directory) was not found."Resource doesn't exist
6ALREADY_EXISTS"The entity that a client attempted to create already exists."Duplicate unique key
7PERMISSION_DENIED"The caller does not have permission to execute the specified operation."Authenticated but unauthorised
8RESOURCE_EXHAUSTED"Some resource has been exhausted, perhaps a per-user quota."Rate limit hit
9FAILED_PRECONDITION"System is not in a state required for the operation's execution... client should not retry until the system state has been explicitly fixed."Delete non-empty bucket
10ABORTED"The operation was aborted, typically due to a concurrency issue such as a sequencer check failure or transaction abort."Optimistic-locking conflict
11OUT_OF_RANGE"The operation was attempted past the valid range... problem that may be fixed if the system state changes."Seek past EOF
12UNIMPLEMENTED"The operation is not implemented or is not supported/enabled in this service."Method not in this version
13INTERNAL"Internal errors... invariants expected by the underlying system have been broken."Database corruption
14UNAVAILABLE"The service is currently unavailable... most likely a transient condition, which can be corrected by retrying with a backoff."Backend restarting
15DATA_LOSS"Unrecoverable data loss or corruption."Storage volume failed
16UNAUTHENTICATED"The request does not have valid authentication credentials for the operation."Missing JWT

Retry behaviour

Per AIP-194 (opens in new window):

CodeRetry?Notes
OKn/aSuccess
CANCELLEDNoClient requested cancellation; honour it
UNKNOWNNoUnsafe; retrying may compound state
INVALID_ARGUMENTNoArgument won't change
DEADLINE_EXCEEDEDNoApplication deadline must be respected
NOT_FOUNDNoRequires state change
ALREADY_EXISTSNoRequires state change
PERMISSION_DENIEDNoRequires permission change
RESOURCE_EXHAUSTEDMaybeQuota may take hours; consider billing
FAILED_PRECONDITIONNo"Client should not retry until the system state has been explicitly fixed" (gRPC docs)
ABORTEDApplication-level"Retry at the transaction level, not individual request level"
OUT_OF_RANGENoRequires state change
UNIMPLEMENTEDNoMethod doesn't exist
INTERNALNoSurface bugs immediately
UNAVAILABLEYes"The only error code explicitly recommended for automatic retry" per AIP-194
DATA_LOSSNoUnrecoverable; surface immediately
UNAUTHENTICATEDNoRe-auth first, then retry application-level

Client-side retry policy

{
  "methodConfig": [{
    "name": [{"service": "example.v1.UserService"}],
    "retryPolicy": {
      "maxAttempts": 4,
      "initialBackoff": "0.1s",
      "maxBackoff": "1s",
      "backoffMultiplier": 2,
      "retryableStatusCodes": ["UNAVAILABLE"]
    }
  }]
}

Adding RESOURCE_EXHAUSTED to retryableStatusCodes is sometimes seen but per AIP-194 has billing implications.

gRPC ↔ HTTP mapping (grpc-gateway)

Per grpc-gateway/runtime/errors.go (opens in new window):

gRPC codeHTTP statusNotes
OK200Standard success
CANCELLED499Client Closed Request (nginx-originated, non-standard)
UNKNOWN500Internal Server Error
INVALID_ARGUMENT400Bad Request
DEADLINE_EXCEEDED504Gateway Timeout
NOT_FOUND404Not Found
ALREADY_EXISTS409Conflict
PERMISSION_DENIED403Forbidden
UNAUTHENTICATED401Unauthorized
RESOURCE_EXHAUSTED429Too Many Requests
FAILED_PRECONDITION400Bad Request - NOT 412 "Precondition Failed" despite the name. grpc-gateway code comment: "deliberately doesn't translate to the similarly named '412 Precondition Failed'"
ABORTED409Conflict (concurrency)
OUT_OF_RANGE400Bad Request
UNIMPLEMENTED501Not Implemented
INTERNAL500Internal Server Error
UNAVAILABLE503Service Unavailable
DATA_LOSS500Internal Server Error

Implications for HTTP clients

  • UNAUTHENTICATED (401) vs PERMISSION_DENIED (403) - same distinction as plain HTTP.
  • INVALID_ARGUMENT, FAILED_PRECONDITION, OUT_OF_RANGE all → 400; the gRPC code carries the semantic distinction.
  • INTERNAL, UNKNOWN, DATA_LOSS all → 500.
  • HTTP 499 (Cancelled) is non-standard; some HTTP clients don't handle it.

Choosing the right code - disambiguators

The hardest pairs:

INVALID_ARGUMENT vs FAILED_PRECONDITION vs OUT_OF_RANGE

QuestionAnswer
Is the arg malformed regardless of system state?INVALID_ARGUMENT
Is the arg valid but the system isn't in the right state?FAILED_PRECONDITION
Is the arg past a valid range that may shift over time?OUT_OF_RANGE

Per gRPC docs: FAILED_PRECONDITION "client should not retry"; OUT_OF_RANGE "may be fixed if the system state changes."

FAILED_PRECONDITION vs ABORTED vs UNAVAILABLE

QuestionAnswer
Concurrency conflict (transaction abort, sequencer check)?ABORTED
System needs state change before retry?FAILED_PRECONDITION
System transient unavailability?UNAVAILABLE

Per AIP-194: ABORTED retries at the transaction level, not the request level. UNAVAILABLE retries at the request level.

NOT_FOUND vs UNIMPLEMENTED

QuestionAnswer
Resource doesn't exist at this point in time?NOT_FOUND
Method doesn't exist in this server version?UNIMPLEMENTED

UNKNOWN vs INTERNAL

QuestionAnswer
Server caught a non-gRPC error and is wrapping it?UNKNOWN
Server detected its own invariant violation?INTERNAL

UNKNOWN is for upstream noise; INTERNAL is for "I detected something wrong with me."

Tests should assert these codes

In client tests (per grpcurl-cli and language-specific clients):

import grpc
import pytest

def test_get_user_not_found_returns_not_found(stub):
    with pytest.raises(grpc.RpcError) as exc_info:
        stub.GetUser(GetUserRequest(id="nonexistent"))
    assert exc_info.value.code() == grpc.StatusCode.NOT_FOUND

def test_create_user_with_duplicate_email_returns_already_exists(stub, existing_user):
    with pytest.raises(grpc.RpcError) as exc_info:
        stub.CreateUser(CreateUserRequest(email=existing_user.email))
    assert exc_info.value.code() == grpc.StatusCode.ALREADY_EXISTS

def test_unauthenticated_call_returns_unauthenticated(stub_no_auth):
    with pytest.raises(grpc.RpcError) as exc_info:
        stub_no_auth.GetUser(GetUserRequest(id="any"))
    assert exc_info.value.code() == grpc.StatusCode.UNAUTHENTICATED

Anti-patterns

Anti-patternWhy it failsFix
Returning INTERNAL for caller-side errorsSurfaces server bugs that don't exist; alarms fireUse INVALID_ARGUMENT for bad inputs
Returning UNKNOWN everywhereNo retry semantics, no HTTP mapping clarityPick the specific code
Returning FAILED_PRECONDITION for transient unavailabilityClients don't retry → user-visible failuresUse UNAVAILABLE
Using NOT_FOUND for "you don't have permission"Information leak (tenant probes) - OR - opacity (debugging hard); document the choicePer tenant-leak-test-author (in the qa-multi-tenancy plugin) the 404-vs-403 trade-off is project policy
OK with error message in payloadBreaks every gRPC client's error handlingAlways use a non-OK status for errors
Adding all codes to retryableStatusCodesRetries non-idempotent operations; data corruptionOnly UNAVAILABLE per AIP-194 (case-by-case for others)
Treating HTTP 412 as FAILED_PRECONDITION in gatewaygrpc-gateway maps FP→400 (not 412)Verify the mapping in runtime/errors.go
Asserting on error message string in testsBreaks on i18n / wording tweaksAssert on code() only

Custom error details

For richer error info beyond the code, embed google.rpc.ErrorInfo / BadRequest / Help in status.Details:

from google.rpc import error_details_pb2, status_pb2
from grpc_status import rpc_status

def get_user_with_details(stub, user_id):
    try:
        return stub.GetUser(GetUserRequest(id=user_id))
    except grpc.RpcError as e:
        status = rpc_status.from_call(e)
        for detail in status.details:
            if detail.Is(error_details_pb2.BadRequest.DESCRIPTOR):
                bad_req = error_details_pb2.BadRequest()
                detail.Unpack(bad_req)
                for v in bad_req.field_violations:
                    print(f"{v.field}: {v.description}")

Tests should assert on detail messages where richer signal exists.

Limitations

  • No standard payload for codes. Each service defines its own "structured details" envelope (Google uses google.rpc.*; others roll their own).
  • HTTP mapping is gateway-specific. envoy, grpc-gateway, and hand-rolled adapters can differ. The grpc-gateway table above is the most common reference.
  • UNKNOWN is overused in the wild. Defensive servers default to UNKNOWN to avoid leaking internals; this defeats observability.
  • Status code metrics. Dashboards that aggregate by code lose information when servers route everything through INTERNAL.

References

Related skills

buf-cli-lint-breaking-build

Wraps the buf CLI for protobuf PR gating: `buf build` (compile .proto), `buf lint` (STANDARD rules: snake_case fields, Service suffix), `buf breaking --against {ref}` (detect wire/codegen breakage vs a git/BSR baseline), and `buf format`. Use as the CI proto-lint + breaking-change gate, or to debug a breaking failure by rule ID (e.g. FIELD_NO_DELETE_UNLESS_NUMBER_RESERVED) and pick the FILE/PACKAGE/WIRE_JSON/WIRE ruleset per consumer. This is the detection TOOL that enforces the rules; for the catalog of what is breaking and why use protobuf-versioning-strategy-reference, and for cross-service schema contract testing use protobuf-compat-checking - not this.

ghz-load

Wraps ghz, the gRPC load testing tool, for throughput and latency benchmarking. Covers test invocation (--proto + --call + host:port; or --protoset for compiled descriptors), load parameters (-n total requests, -c concurrency, -r RPS rate limit, -z duration), output formats (json/csv/html/influx-summary for CI consumption), the metrics reported (RPS achieved, latency p50/p95/p99, status-code distribution, errors), and CI integration patterns for regression gating. Use when benchmarking a gRPC service's throughput or detecting latency regressions in CI.

grpc-interceptor-test-author

Authors unit tests for gRPC interceptor logic: Go grpc.UnaryServerInterceptor/UnaryClientInterceptor, Java ServerInterceptor/ClientInterceptor, and grpc-js client interceptors. Covers auth (Unauthenticated on bad token), retry (backoff on Unavailable), logging/tracing (metadata extraction + propagation), error-mapping (status translation), and chained interceptor ordering - by calling the interceptor directly with a spy handler, no live backend. Use when a gRPC interceptor is written or modified. Different test surface from grpc-streaming-test-author (multi-message stream sequences) and grpc-mock (service handler logic) - use those, not this, for streams or handlers.

grpc-mock

Wraps gRPC server-mocking patterns for client-side tests: Go bufconn (in-memory net.Listener via google.golang.org/grpc/test/bufconn) + mockgen-generated interface mocks, Python pytest-grpc fixtures + unittest.mock patching of stubs, JVM grpc-mock library / in-process gRPC server (InProcessServerBuilder), Node @grpc/grpc-js fake server with NewServer-on-port-0. Use when writing client-side tests that need a controllable gRPC server response (success cases, error cases per grpc-status-code-mapping-reference, timeouts, and single-response error injection) without spinning up a real backend. For multi-message streaming-sequence tests (server-streaming, bidi), use grpc-streaming-test-author instead. Distinct from grpcurl-cli (ad-hoc CLI invocation against a real server) and ghz-load (perf against a real server).

grpc-streaming-test-author

Workflow-driven skill that builds gRPC streaming-RPC test suites from a proto definition. Classifies each RPC by pattern (unary, server-streaming, client-streaming, bidi), then emits the required categories per pattern - ordering preservation, completion semantics (server close after stream end, client half-close), cancellation, deadline handling, partial-stream failure. Produces skeletons for Go (bufconn + Send/Recv), Python (iterators), JVM (StreamObserver), Node (call.write/end). Use when adding tests for a new streaming RPC or auditing a suite for uncovered categories. Different test surface from grpc-interceptor-test-author (interceptor layer) and grpc-mock (harness); for wire-level streaming semantics use grpc-streaming-tests, not this.

grpcurl-cli

Wraps grpcurl, the curl-equivalent CLI for gRPC. Covers descriptor sources (server reflection default, --import-path + --proto for proto files, --protoset for compiled descriptor sets), service discovery (`list`, `describe`), invoking unary RPCs (`-d '{...}'`, `-d @file.json`, `-d @` for stdin), streaming RPCs (newline-delimited JSON via stdin), TLS configuration (--cacert, --cert, --key, --insecure, --plaintext), header injection (-H 'Authorization: Bearer ...'), and exit codes. Use for ad-hoc gRPC debugging, smoke testing, scriptable PR-time gates, and CLI-based interaction with reflective gRPC services.

protobuf-versioning-strategy-reference

Pure-reference catalog of protobuf3 versioning and breaking-change rules: field-number reservation (reserve on delete; 1..536870911; 19000-19999 reserved), wire-safe vs wire-incompatible changes (add/remove safe with reservation; changing a field number always breaks), compatible type conversions (int32/uint32/int64/uint64/bool; sint32/sint64; string/bytes for UTF-8; enum/int), oneof + map constraints, and buf's four breaking categories (FILE/PACKAGE/WIRE_JSON/WIRE) with rule IDs. Use when designing a schema change or picking a buf breaking ruleset. This is the catalog of what is breaking and why, not a scanner; to detect changes in CI use buf-cli-lint-breaking-build, for the gRPC status-code vocabulary use grpc-status-code-mapping-reference, and for cross-service contract testing use protobuf-compat-checking.