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.

grpc-status-code-mapping-reference

Overview

gRPC defines 17 standard status codes (per grpc.io/docs/guides/status-codes/) 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, only UNAVAILABLE is generally safe to auto-retry.
  3. The HTTP mapping - per grpc-gateway runtime/errors.go, 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/:

#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:

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:

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 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