CtrlK
BlogDocsLog inGet started
Tessl Logo

testland/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.

75

Quality

94%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Passed

No findings from the security scan

Overview
Quality
Evals
Security
Files

SKILL.md

name:
grpc-status-code-mapping-reference
description:
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 (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

SKILL.md

tile.json