Skip to main content

structured_proxy/transcode/
error.rs

1//! gRPC → HTTP error mapping.
2//!
3//! Converts `tonic::Status` to appropriate HTTP status codes and JSON error bodies
4//! following the gRPC-HTTP status code mapping from the gRPC specification.
5
6use axum::http::StatusCode;
7use axum::response::{IntoResponse, Response};
8use axum::Json;
9
10/// Map a gRPC status code to the corresponding HTTP status code.
11///
12/// Based on <https://github.com/grpc/grpc/blob/master/doc/http-grpc-status-mapping.md>
13pub fn grpc_to_http_status(code: tonic::Code) -> StatusCode {
14    match code {
15        tonic::Code::Ok => StatusCode::OK,
16        tonic::Code::Cancelled => StatusCode::from_u16(499).unwrap_or(StatusCode::BAD_REQUEST),
17        tonic::Code::Unknown => StatusCode::INTERNAL_SERVER_ERROR,
18        tonic::Code::InvalidArgument => StatusCode::BAD_REQUEST,
19        tonic::Code::DeadlineExceeded => StatusCode::GATEWAY_TIMEOUT,
20        tonic::Code::NotFound => StatusCode::NOT_FOUND,
21        tonic::Code::AlreadyExists => StatusCode::CONFLICT,
22        tonic::Code::PermissionDenied => StatusCode::FORBIDDEN,
23        tonic::Code::ResourceExhausted => StatusCode::TOO_MANY_REQUESTS,
24        tonic::Code::FailedPrecondition => StatusCode::BAD_REQUEST,
25        tonic::Code::Aborted => StatusCode::CONFLICT,
26        tonic::Code::OutOfRange => StatusCode::BAD_REQUEST,
27        tonic::Code::Unimplemented => StatusCode::NOT_IMPLEMENTED,
28        tonic::Code::Internal => StatusCode::INTERNAL_SERVER_ERROR,
29        tonic::Code::Unavailable => StatusCode::SERVICE_UNAVAILABLE,
30        tonic::Code::DataLoss => StatusCode::INTERNAL_SERVER_ERROR,
31        tonic::Code::Unauthenticated => StatusCode::UNAUTHORIZED,
32    }
33}
34
35/// Convert a `tonic::Status` into an axum HTTP response with JSON error body.
36pub fn status_to_response(status: tonic::Status) -> Response {
37    let http_status = grpc_to_http_status(status.code());
38    let body = serde_json::json!({
39        "error": grpc_code_name(status.code()),
40        "message": status.message(),
41        "code": status.code() as i32,
42    });
43    (http_status, Json(body)).into_response()
44}
45
46/// Human-readable gRPC code name for JSON error responses.
47fn grpc_code_name(code: tonic::Code) -> &'static str {
48    match code {
49        tonic::Code::Ok => "OK",
50        tonic::Code::Cancelled => "CANCELLED",
51        tonic::Code::Unknown => "UNKNOWN",
52        tonic::Code::InvalidArgument => "INVALID_ARGUMENT",
53        tonic::Code::DeadlineExceeded => "DEADLINE_EXCEEDED",
54        tonic::Code::NotFound => "NOT_FOUND",
55        tonic::Code::AlreadyExists => "ALREADY_EXISTS",
56        tonic::Code::PermissionDenied => "PERMISSION_DENIED",
57        tonic::Code::ResourceExhausted => "RESOURCE_EXHAUSTED",
58        tonic::Code::FailedPrecondition => "FAILED_PRECONDITION",
59        tonic::Code::Aborted => "ABORTED",
60        tonic::Code::OutOfRange => "OUT_OF_RANGE",
61        tonic::Code::Unimplemented => "UNIMPLEMENTED",
62        tonic::Code::Internal => "INTERNAL",
63        tonic::Code::Unavailable => "UNAVAILABLE",
64        tonic::Code::DataLoss => "DATA_LOSS",
65        tonic::Code::Unauthenticated => "UNAUTHENTICATED",
66    }
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72
73    #[test]
74    fn test_grpc_to_http_mapping() {
75        assert_eq!(grpc_to_http_status(tonic::Code::Ok), StatusCode::OK);
76        assert_eq!(
77            grpc_to_http_status(tonic::Code::InvalidArgument),
78            StatusCode::BAD_REQUEST
79        );
80        assert_eq!(
81            grpc_to_http_status(tonic::Code::NotFound),
82            StatusCode::NOT_FOUND
83        );
84        assert_eq!(
85            grpc_to_http_status(tonic::Code::AlreadyExists),
86            StatusCode::CONFLICT
87        );
88        assert_eq!(
89            grpc_to_http_status(tonic::Code::PermissionDenied),
90            StatusCode::FORBIDDEN
91        );
92        assert_eq!(
93            grpc_to_http_status(tonic::Code::Unauthenticated),
94            StatusCode::UNAUTHORIZED
95        );
96        assert_eq!(
97            grpc_to_http_status(tonic::Code::ResourceExhausted),
98            StatusCode::TOO_MANY_REQUESTS
99        );
100        assert_eq!(
101            grpc_to_http_status(tonic::Code::Unimplemented),
102            StatusCode::NOT_IMPLEMENTED
103        );
104        assert_eq!(
105            grpc_to_http_status(tonic::Code::Internal),
106            StatusCode::INTERNAL_SERVER_ERROR
107        );
108        assert_eq!(
109            grpc_to_http_status(tonic::Code::Unavailable),
110            StatusCode::SERVICE_UNAVAILABLE
111        );
112        assert_eq!(
113            grpc_to_http_status(tonic::Code::DeadlineExceeded),
114            StatusCode::GATEWAY_TIMEOUT
115        );
116    }
117
118    #[test]
119    fn test_grpc_code_name() {
120        assert_eq!(grpc_code_name(tonic::Code::Ok), "OK");
121        assert_eq!(grpc_code_name(tonic::Code::NotFound), "NOT_FOUND");
122        assert_eq!(
123            grpc_code_name(tonic::Code::Unauthenticated),
124            "UNAUTHENTICATED"
125        );
126    }
127
128    #[test]
129    fn test_status_to_response() {
130        let status = tonic::Status::not_found("user not found");
131        let response = status_to_response(status);
132        assert_eq!(response.status(), StatusCode::NOT_FOUND);
133    }
134}