Mapping HTTP status codes to gRPC status codes

1 Introduction

When a service moves between HTTP and gRPC the status codes and errors will change. 
HTTP uses numeric response status codes. gRPC uses a small set of canonical status codes. 
The objective here is practical. Cover every gRPC code defined by Google and give clear Java code you can drop into a project. 
The canonical list comes from the Google code.proto.
A handy reference that pairs common HTTP codes to gRPC codes is also useful.

This guide shows:

  • a mapping from HTTP -> gRPC for the common but also edge cases
  • a mapping from gRPC -> HTTP
  • some java helpers with a couple of notes

This will help you keep the mapping stable across your services.

2 Mapping Principles

  1. Prefer intent over numeric similarity. Choose the gRPC code that best signals what the user/caller should do. For example, input problems should map to argument errors.
  2. Preserve retry semantics. If an http response signals failure, map it to UNAVAILABLE or DEADLINE_EXCEEDED.
  3. Keep authentication and authorization distinct. Map missing or invalid credentials to UNAUTHENTICATED. Map valid credentials but forbidden action to PERMISSION_DENIED.
  4. For ambiguous http status codes, decide by context and make that decision explicit in code or via an enum flag.

3 Complete gRPC list from code.proto

  • OK = 0
  • CANCELLED = 1
  • UNKNOWN = 2
  • INVALID_ARGUMENT = 3
  • DEADLINE_EXCEEDED = 4
  • NOT_FOUND = 5
  • ALREADY_EXISTS = 6
  • PERMISSION_DENIED = 7
  • RESOURCE_EXHAUSTED = 8
  • FAILED_PRECONDITION = 9
  • ABORTED = 10
  • OUT_OF_RANGE = 11
  • UNIMPLEMENTED = 12
  • INTERNAL = 13
  • UNAVAILABLE = 14
  • DATA_LOSS = 15
  • UNAUTHENTICATED = 16

Below are practical HTTP -> gRPC mappings that handle both normal and less common situations.

4 Recommended HTTP -> gRPC mapping

HTTP code(s) gRPC code Reason
200, 201, 204 OK Success. Return the payload or empty.
400 INVALID_ARGUMENT Client sent malformed or invalid data.
401 UNAUTHENTICATED Credentials missing or invalid.
403 PERMISSION_DENIED Authenticated but not allowed.
404 NOT_FOUND Requested resource does not exist.
409 ABORTED or ALREADY_EXISTS Use ALREADY_EXISTS when an id or key already exists. Use ABORTED for transactional abort or concurrent modification.
412 FAILED_PRECONDITION Preconditions or state checks failed.
413 INVALID_ARGUMENT Payload too large to process as given.
415 INVALID_ARGUMENT Unsupported media type.
416 OUT_OF_RANGE Range request outside available bounds.
429 RESOURCE_EXHAUSTED Rate limit or quota exceeded.
499 CANCELLED Client closed connection. Map to CANCELLED.
500 INTERNAL or UNKNOWN or DATA_LOSS Use INTERNAL for server bugs. Use DATA_LOSS only when data corruption is suspected. Use UNKNOWN as a general fallback.
501 UNIMPLEMENTED Endpoint not implemented.
502 UNAVAILABLE Bad gateway. Consider UNAVAILABLE for upstream failure.
503 UNAVAILABLE Service temporarily unavailable.
504 DEADLINE_EXCEEDED Gateway timeout or overall deadline exceeded.
any other 5xx INTERNAL Server side error of unspecified kind.

5 Java Mapper

Below are 2 small helpers. One conversts HTTP status to gRPC. And you guessed it, one does it the other way around.

Use io.grpc.Status and io.grpc.StatusRuntimeException to propagate the error through gRPC layers.

import io.grpc.Status;
import io.grpc.StatusRuntimeException;

public final class HttpGrpcStatus {

    private HttpGrpcStatus() {}

    public static Status fromHttp(int httpStatus) {
        switch (httpStatus) {
            case 200:
            case 201:
            case 204:
                return Status.OK;
            case 400:
                return Status.INVALID_ARGUMENT;
            case 401:
                return Status.UNAUTHENTICATED;
            case 403:
                return Status.PERMISSION_DENIED;
            case 404:
                return Status.NOT_FOUND;
            case 409:
                // default choice. Callers can refine to ALREADY_EXISTS or ABORTED.
                return Status.ABORTED;
            case 412:
                return Status.FAILED_PRECONDITION;
            case 413:
            case 415:
                return Status.INVALID_ARGUMENT;
            case 416:
                return Status.OUT_OF_RANGE;
            case 429:
                return Status.RESOURCE_EXHAUSTED;
            case 499:
                return Status.CANCELLED;
            case 500:
                return Status.INTERNAL;
            case 501:
                return Status.UNIMPLEMENTED;
            case 502:
                return Status.UNAVAILABLE;
            case 503:
                return Status.UNAVAILABLE;
            case 504:
                return Status.DEADLINE_EXCEEDED;
            default:
                if (httpStatus >= 400 && httpStatus < 500) {
                    return Status.INVALID_ARGUMENT;
                }
                if (httpStatus >= 500 && httpStatus < 600) {
                    return Status.INTERNAL;
                }
                return Status.UNKNOWN;
        }
    }

    public static int toHttp(Status.Code grpcCode) {
        switch (grpcCode) {
            case OK:
                return 200;
            case CANCELLED:
                return 499; // client closed request
            case UNKNOWN:
                return 500;
            case INVALID_ARGUMENT:
                return 400;
            case DEADLINE_EXCEEDED:
                return 504;
            case NOT_FOUND:
                return 404;
            case ALREADY_EXISTS:
                return 409;
            case PERMISSION_DENIED:
                return 403;
            case RESOURCE_EXHAUSTED:
                return 429;
            case FAILED_PRECONDITION:
                return 412;
            case ABORTED:
                return 409;
            case OUT_OF_RANGE:
                return 416;
            case UNIMPLEMENTED:
                return 501;
            case INTERNAL:
                return 500;
            case UNAVAILABLE:
                return 503;
            case DATA_LOSS:
                return 500;
            case UNAUTHENTICATED:
                return 401;
            default:
                return 500;
        }
    }

    public static void throwIfNotOk(int httpStatus) {
        Status status = fromHttp(httpStatus);
        if (!status.isOk()) {
            throw status.asRuntimeException();
        }
    }
}

Some notes:

  • 409 maps to ABORTED by default. If you know the conflict is an existing resource, you may want to use ALREADY_EXISTS
  • for 499 use CANCELLED

6 Where to apply the mapper

  1. At service boundaries. Convert once when crossing from HTTP to gRPC or back. Keep the rest of your code base in the native error model
  2. For gateway layers that translate between REST and gRPC, log the original HTTP code and any original error payload. Include enough context so callers can debug without guessing
  3. For multi-step flows, annotate status values with machine-readable details when possible. Use com.google.rpc.Status details if you need structured error metadata

7 Quick guidelines for ambiguous cases

  • 409: If the server can determine the exact cause and it is an existing identifier choose ALREADY_EXISTS. If the conflict is transactional or concurrent operation prefer ABORTED
  • 429: Map to RESOURCE_EXHAUSTED and consider adding Retry-After or a structured field indicating quota and reset time
  • 5xx: Prefer INTERNAL for code bugs. Use UNAVAILABLE for temporary outages and DEADLINE_EXCEEDED when a timeout has occurred
  • Authentication vs permission: UNAUTHENTICATED means the caller did not present valid credentials. PERMISSION_DENIED means credentials are valid but access is not allowed

Short example:

HttpResponse res = restClient.call(endpoint);
Status status = HttpGrpcStatus.fromHttp(res.statusCode());
if (!status.isOk()) {
    // include body text or structured details if available
    throw status.withDescription(res.body()).asRuntimeException();
}

8 Conclusion

This mapping follows the canonical gRPC codes while preserving the meaning of the original http status codes. Use the table above as the default.

When you must deviate, document the exception at the service boundary.

For a complete list of canonical gRPC codes see code.proto.