subliminal_protos_rust/
errors.rs

1use std::{error::Error, fmt::Display};
2
3use axum::{
4    http::StatusCode,
5    response::{IntoResponse, Response},
6    Json,
7};
8use serde_json::{json, Value};
9use tonic::Status;
10
11/// Error types related to a particular service (not the API itself).
12#[derive(Debug)]
13pub enum ServiceErrors {
14    // When a service indicates a resource is not found.
15    NotFound(Status),
16
17    // When a service is unavailable.
18    ServiceUnavailable(Status),
19
20    // When a service returns an internal error.
21    ServiceInternal(Status),
22}
23
24// Implement From<tonic::Status> so that we can convert a tonic::Status into a ServiceErrors variant.
25// This is useful for translating service errors into eventual HTTP responses.
26// We need a separate error enum due to the orphan rule.
27impl From<tonic::Status> for ServiceErrors {
28    fn from(status: tonic::Status) -> Self {
29        match status.code() {
30            tonic::Code::NotFound => ServiceErrors::NotFound(status),
31            tonic::Code::Unavailable => ServiceErrors::ServiceUnavailable(status),
32            _ => ServiceErrors::ServiceInternal(status),
33        }
34    }
35}
36
37/// This happens when a client is unable to connect to a service.
38impl From<tonic::transport::Error> for ServiceErrors {
39    fn from(error: tonic::transport::Error) -> Self {
40        ServiceErrors::ServiceUnavailable(Status::new(tonic::Code::Unavailable, error.to_string()))
41    }
42}
43
44impl Error for ServiceErrors {}
45impl Display for ServiceErrors {
46    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47        match self {
48            ServiceErrors::NotFound(e) => {
49                write!(f, "Service indicated resource was not found: {}", e)
50            }
51            ServiceErrors::ServiceUnavailable(e) => {
52                write!(f, "Service is unavailable: {}", e)
53            }
54            ServiceErrors::ServiceInternal(e) => {
55                write!(f, "Service encountered an internal error: {}", e)
56            }
57        }
58    }
59}
60
61/// Implement IntoResponse for HttpResponse so that we can return it from an axum handler.
62/// This is primarily used for outside services that the API proxies requests to, whereas the API itself
63/// returns HttpResponses directly.
64impl IntoResponse for ServiceErrors {
65    fn into_response(self) -> Response {
66        match self {
67            // When this error is encountered, it means a service was unable to find a resource.
68            // There are cases where we want to override this behavior, such as when we want to return
69            // a 503 status code instead of a 404 when trying to get a service location from the registry.
70            ServiceErrors::NotFound(e) => {
71                (StatusCode::NOT_FOUND, error_json(e.message().to_string()))
72            }
73            // When this error is encountered, it means we were unable to connect to a particular service.
74            // Instead of returning a 503 (indicating issues with the API itself), we return a 504 to indicate
75            // there is an issue with proxied services.
76            ServiceErrors::ServiceUnavailable(e) => (
77                StatusCode::GATEWAY_TIMEOUT,
78                error_json(e.message().to_string()),
79            ),
80            // When this error is encountered, it means a service (not the API) encountered an internal error.
81            ServiceErrors::ServiceInternal(e) => {
82                (StatusCode::BAD_GATEWAY, error_json(e.message().to_string()))
83            }
84        }
85        .into_response()
86    }
87}
88
89/// Quick helper to convert an error string into a JSON object.
90fn error_json(error: String) -> Json<Value> {
91    Json(json!({ "error": error }))
92}
93