Skip to main content

gestalt/public/
errors.rs

1//! Gestalt public transport errors.
2
3use crate::rpc_support::{GestaltError, gestalt_error_code, http_status_to_gestalt_code};
4
5/// Header name that marks OperationResult passthrough responses.
6pub const RESPONSE_KIND_HEADER: &str = "X-Gestalt-Response-Kind";
7/// Header value indicating the body is an OperationResult envelope.
8pub const RESPONSE_KIND_OPERATION_RESULT: &str = "operation-result";
9
10/// Reports whether headers mark an OperationResult passthrough.
11pub fn is_operation_result(headers: &http::HeaderMap) -> bool {
12    headers
13        .get(RESPONSE_KIND_HEADER)
14        .and_then(|value| value.to_str().ok())
15        == Some(RESPONSE_KIND_OPERATION_RESULT)
16}
17
18/// Parses a non-operation-result REST error body when possible.
19pub fn parse_gateway_error(status: u16, body: &[u8]) -> GestaltError {
20    let parsed = if body.is_empty() {
21        None
22    } else {
23        serde_json::from_slice::<serde_json::Value>(body).ok()
24    };
25    let code = resolve_gateway_error_code(status, parsed.as_ref());
26    let message = parsed
27        .as_ref()
28        .and_then(extract_gateway_error_message)
29        .unwrap_or_else(|| format!("request failed with status {status}"));
30    GestaltError::new(code, message)
31}
32
33fn extract_gateway_error_message(parsed: &serde_json::Value) -> Option<String> {
34    for key in ["message", "error"] {
35        if let Some(text) = parsed.get(key).and_then(|value| value.as_str()) {
36            let text = text.trim();
37            if !text.is_empty() {
38                return Some(text.to_string());
39            }
40        }
41    }
42    None
43}
44
45fn resolve_gateway_error_code(status: u16, parsed: Option<&serde_json::Value>) -> i32 {
46    let status = status as i32;
47    let Some(parsed) = parsed else {
48        return http_status_to_gestalt_code(status);
49    };
50    match parsed.get("code") {
51        Some(serde_json::Value::Number(number)) => number
52            .as_i64()
53            .and_then(|value| i32::try_from(value).ok())
54            .unwrap_or_else(|| http_status_to_gestalt_code(status)),
55        Some(serde_json::Value::String(name)) => grpc_code_name_to_gestalt_code(name)
56            .unwrap_or_else(|| http_status_to_gestalt_code(status)),
57        _ => http_status_to_gestalt_code(status),
58    }
59}
60
61fn grpc_code_name_to_gestalt_code(name: &str) -> Option<i32> {
62    Some(match name {
63        "Canceled" => gestalt_error_code::CANCELLED,
64        "Unknown" => gestalt_error_code::UNKNOWN,
65        "InvalidArgument" => gestalt_error_code::INVALID_ARGUMENT,
66        "DeadlineExceeded" => gestalt_error_code::DEADLINE_EXCEEDED,
67        "NotFound" => gestalt_error_code::NOT_FOUND,
68        "AlreadyExists" => gestalt_error_code::ALREADY_EXISTS,
69        "PermissionDenied" => gestalt_error_code::PERMISSION_DENIED,
70        "ResourceExhausted" => gestalt_error_code::RESOURCE_EXHAUSTED,
71        "FailedPrecondition" => gestalt_error_code::FAILED_PRECONDITION,
72        "Aborted" => gestalt_error_code::ABORTED,
73        "OutOfRange" => gestalt_error_code::OUT_OF_RANGE,
74        "Unimplemented" => gestalt_error_code::UNIMPLEMENTED,
75        "Internal" => gestalt_error_code::INTERNAL,
76        "Unavailable" => gestalt_error_code::UNAVAILABLE,
77        "DataLoss" => gestalt_error_code::DATA_LOSS,
78        "Unauthenticated" => gestalt_error_code::UNAUTHENTICATED,
79        _ => return None,
80    })
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    #[test]
88    fn parses_gateway_error_field_and_string_code() {
89        let body = br#"{"code":"Unauthenticated","error":"Not authenticated"}"#;
90        let err = parse_gateway_error(401, body);
91        assert_eq!(err.code, gestalt_error_code::UNAUTHENTICATED);
92        assert_eq!(err.message, "Not authenticated");
93    }
94}