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
- 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.
- Preserve retry semantics. If an http response signals failure, map it to
UNAVAILABLEorDEADLINE_EXCEEDED. - Keep authentication and authorization distinct. Map missing or invalid credentials to
UNAUTHENTICATED. Map valid credentials but forbidden action toPERMISSION_DENIED. - 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
ABORTEDby default. If you know the conflict is an existing resource, you may want to useALREADY_EXISTS - for 499 use CANCELLED
6 Where to apply the mapper
- 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
- 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
- For multi-step flows, annotate status values with machine-readable details when possible. Use
com.google.rpc.Statusdetails 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 preferABORTED - 429: Map to
RESOURCE_EXHAUSTEDand consider addingRetry-Afteror a structured field indicating quota and reset time - 5xx: Prefer
INTERNALfor code bugs. UseUNAVAILABLEfor temporary outages andDEADLINE_EXCEEDEDwhen a timeout has occurred - Authentication vs permission:
UNAUTHENTICATEDmeans the caller did not present valid credentials.PERMISSION_DENIEDmeans 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.