Skip to main content

tako_rs_core/grpc/
status.rs

1//! gRPC status codes, the trailer `GrpcStatus` payload, and error-response
2//! construction shared across the unary and streaming responders.
3
4use http::HeaderMap;
5use http::StatusCode;
6
7use crate::body::TakoBody;
8use crate::types::Response;
9
10/// gRPC status codes.
11///
12/// See <https://grpc.github.io/grpc/core/md_doc_statuscodes.html>
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14#[repr(u8)]
15pub enum GrpcStatusCode {
16  Ok = 0,
17  Cancelled = 1,
18  Unknown = 2,
19  InvalidArgument = 3,
20  DeadlineExceeded = 4,
21  NotFound = 5,
22  AlreadyExists = 6,
23  PermissionDenied = 7,
24  ResourceExhausted = 8,
25  FailedPrecondition = 9,
26  Aborted = 10,
27  OutOfRange = 11,
28  Unimplemented = 12,
29  Internal = 13,
30  Unavailable = 14,
31  DataLoss = 15,
32  Unauthenticated = 16,
33}
34
35/// Percent-encode a gRPC `Status-Message` per PROTOCOL-HTTP2.md.
36///
37/// The spec preserves visible ASCII (`0x20..=0x7E`) except `%`, and
38/// percent-encodes every other byte as `%XX` (upper-case hex). Without
39/// this any non-ASCII character (emoji, accents, Latin-1 upstream error
40/// strings) makes `HeaderValue::from_str` fail and the surrounding
41/// `if let Ok(...)` silently drops the entire `grpc-message` — the
42/// caller would see only `grpc-status` with no human-readable detail.
43fn percent_encode_grpc_message(s: &str) -> String {
44  let mut out = String::with_capacity(s.len());
45  for &b in s.as_bytes() {
46    if (0x20..=0x7E).contains(&b) && b != b'%' {
47      out.push(b as char);
48    } else {
49      out.push('%');
50      out.push(hex_upper(b >> 4));
51      out.push(hex_upper(b & 0x0F));
52    }
53  }
54  out
55}
56
57#[inline]
58fn hex_upper(n: u8) -> char {
59  match n {
60    0..=9 => (b'0' + n) as char,
61    10..=15 => (b'A' + n - 10) as char,
62    _ => unreachable!("hex_upper called with value > 15"),
63  }
64}
65
66pub(crate) fn build_grpc_error_response(status: GrpcStatusCode, message: &str) -> Response {
67  let mut resp = Response::new(TakoBody::empty());
68  *resp.status_mut() = StatusCode::OK; // gRPC always uses 200 OK at HTTP level
69  resp.headers_mut().insert(
70    http::header::CONTENT_TYPE,
71    http::HeaderValue::from_static("application/grpc"),
72  );
73  if let Ok(val) = http::HeaderValue::from_str(&(status as u8).to_string()) {
74    resp.headers_mut().insert("grpc-status", val);
75  }
76  if !message.is_empty()
77    && let Ok(val) = http::HeaderValue::from_str(&percent_encode_grpc_message(message))
78  {
79    resp.headers_mut().insert("grpc-message", val);
80  }
81  resp
82}
83
84/// gRPC status payload (status code + optional message) used in trailers.
85#[derive(Debug, Clone)]
86pub struct GrpcStatus {
87  pub code: GrpcStatusCode,
88  pub message: Option<String>,
89}
90
91impl GrpcStatus {
92  pub fn ok() -> Self {
93    Self {
94      code: GrpcStatusCode::Ok,
95      message: None,
96    }
97  }
98
99  pub fn error(code: GrpcStatusCode, message: impl Into<String>) -> Self {
100    Self {
101      code,
102      message: Some(message.into()),
103    }
104  }
105
106  pub(crate) fn write_trailers(&self) -> HeaderMap {
107    let mut t = HeaderMap::new();
108    if let Ok(v) = http::HeaderValue::from_str(&(self.code as u8).to_string()) {
109      t.insert("grpc-status", v);
110    }
111    if let Some(msg) = self.message.as_deref()
112      && let Ok(v) = http::HeaderValue::from_str(&percent_encode_grpc_message(msg))
113    {
114      t.insert("grpc-message", v);
115    }
116    t
117  }
118}