LCOV - code coverage report
Current view: top level - dashql-native-proxy/src - status.rs Coverage Total Hit
Test: DashQL 6e74948 Lines: 100.0 % 295 295
Test Date: 2026-11-06 17:46:50

            Line data    Source code
       1              : use std::{collections::HashMap, fmt::Error};
       2              : use base64::Engine;
       3              : use serde::Serialize;
       4              : use http::{header::CONTENT_TYPE, Response, StatusCode};
       5              : 
       6              : use crate::proxy_headers::{
       7              :     HEADER_NAME_ERROR, HEADER_NAME_GRPC_MESSAGE, HEADER_NAME_GRPC_STATUS,
       8              :     HEADER_NAME_GRPC_STATUS_DETAILS,
       9              : };
      10              : 
      11              : #[derive(Debug)]
      12              : pub enum GrpcStreamElement {
      13              :     Message,
      14              :     Trailers,
      15              : }
      16              : 
      17              : #[derive(Debug)]
      18              : pub enum Status {
      19              :     GrpcCallFailed{ status: tonic::Status },
      20              :     GrpcChannelIdIsUnknown{ channel_id: usize },
      21              :     GrpcEndpointConnectFailed{ message: String },
      22              :     GrpcTlsConfigInvalid { message: String },
      23              :     GrpcStreamClosed { channel_id: usize, stream_id: usize },
      24              :     GrpcStreamIsUnknown { channel_id: usize, stream_id: usize },
      25              :     GrpcStreamReadFailed { channel_id: usize, stream_id: usize, element: GrpcStreamElement, status: tonic::Status },
      26              :     GrpcStreamReadTimedOut { channel_id: usize, stream_id: usize },
      27              :     HeaderHasInvalidEncoding{ header: &'static str, message: String },
      28              :     HeaderIsNotAValidEndpoint{ header: &'static str, message: String },
      29              :     HeaderIsNotAnUsize{ header: &'static str, message: String },
      30              :     HeaderPathIsInvalid{ header: &'static str, path: String, message: String },
      31              :     HeaderRequiredButMissing{ header: &'static str },
      32              :     HttpClientConfigInvalid{ message: String },
      33              :     HttpEndpointIsInvalid{ header: &'static str, endpoint: String, message: String },
      34              :     HttpMethodIsInvalid{ header: &'static str, method: String, message: String },
      35              :     HttpRequestFailed { stream_id: usize, error: String },
      36              :     HttpStreamClosed { stream_id: usize },
      37              :     HttpStreamFailed { stream_id: usize, error: String },
      38              :     HttpStreamIsUnknown { stream_id: usize },
      39              :     HttpStreamReadFailed { stream_id: usize, error: String },
      40              :     HttpStreamReadTimedOut { stream_id: usize },
      41              :     HttpUrlIsInvalid{ message: String },
      42              : }
      43              : 
      44              : /// The JSON error body sent for a failed proxy request
      45              : #[derive(Serialize)]
      46              : pub struct StatusMessage {
      47              :     /// The human-readable error message
      48              :     message: String,
      49              :     /// Additional error details, e.g. the offending header or the underlying error
      50              :     details: HashMap<&'static str, String>,
      51              : }
      52              : 
      53              : impl TryFrom<&Status> for StatusMessage {
      54              :     type Error = std::fmt::Error;
      55              : 
      56           71 :     fn try_from(s: &Status) -> Result<StatusMessage, Error> {
      57           71 :         match s {
      58            1 :             Status::HeaderHasInvalidEncoding { header, message } => Ok(StatusMessage {
      59            1 :                 message: "header has an invalid encoding".to_string(),
      60            1 :                 details: HashMap::from_iter([
      61            1 :                     ("header", header.to_string()),
      62            1 :                     ("error", message.to_string())
      63            1 :                 ]),
      64            1 :             }),
      65            2 :             Status::HeaderIsNotAValidEndpoint { header, message } => Ok(StatusMessage {
      66            2 :                 message: "header is not a valid endpoint".to_string(),
      67            2 :                 details: HashMap::from_iter([
      68            2 :                     ("header", header.to_string()),
      69            2 :                     ("error", message.to_string())
      70            2 :                 ]),
      71            2 :             }),
      72           15 :             Status::HeaderRequiredButMissing { header } => Ok(StatusMessage {
      73           15 :                 message: "header is missing".to_string(),
      74           15 :                 details: HashMap::from_iter([
      75           15 :                     ("header", header.to_string())
      76           15 :                 ]),
      77           15 :             }),
      78            3 :             Status::HeaderIsNotAnUsize { header, message } => Ok(StatusMessage {
      79            3 :                 message: "header is not an unsigned integer".to_string(),
      80            3 :                 details: HashMap::from_iter([
      81            3 :                     ("header", header.to_string()),
      82            3 :                     ("error", message.to_string())
      83            3 :                 ]),
      84            3 :             }),
      85            3 :             Status::HeaderPathIsInvalid { header, path, message } => Ok(StatusMessage {
      86            3 :                 message: "header stores invalid path".to_string(),
      87            3 :                 details: HashMap::from_iter([
      88            3 :                     ("header", header.to_string()),
      89            3 :                     ("path", path.to_string()),
      90            3 :                     ("error", message.to_string())
      91            3 :                 ]),
      92            3 :             }),
      93            2 :             Status::HttpEndpointIsInvalid { header, endpoint, message } => Ok(StatusMessage {
      94            2 :                 message: "header stores invalid endpoint".to_string(),
      95            2 :                 details: HashMap::from_iter([
      96            2 :                     ("header", header.to_string()),
      97            2 :                     ("endpoint", endpoint.to_string()),
      98            2 :                     ("error", message.to_string())
      99            2 :                 ]),
     100            2 :             }),
     101            2 :             Status::HttpMethodIsInvalid { header, method, message } => Ok(StatusMessage {
     102            2 :                 message: "header stores invalid method".to_string(),
     103            2 :                 details: HashMap::from_iter([
     104            2 :                     ("header", header.to_string()),
     105            2 :                     ("method", method.to_string()),
     106            2 :                     ("error", message.to_string())
     107            2 :                 ]),
     108            2 :             }),
     109            5 :             Status::GrpcChannelIdIsUnknown { channel_id } => Ok(StatusMessage {
     110            5 :                 message: "gRPC channel id is unknown".to_string(),
     111            5 :                 details: HashMap::from_iter([
     112            5 :                     ("channel", channel_id.to_string()),
     113            5 :                 ]),
     114            5 :             }),
     115            3 :             Status::GrpcEndpointConnectFailed { message } => Ok(StatusMessage {
     116            3 :                 message: format!("connecting to gRPC endpoint failed"),
     117            3 :                 details: HashMap::from_iter([
     118            3 :                     ("error", message.to_string()),
     119            3 :                 ]),
     120            3 :             }),
     121            3 :             Status::GrpcTlsConfigInvalid { message } => Ok(StatusMessage {
     122            3 :                 message: "gRPC tls config is invalid".to_string(),
     123            3 :                 details: HashMap::from_iter([
     124            3 :                     ("error", message.to_string()),
     125            3 :                 ]),
     126            3 :             }),
     127            6 :             Status::GrpcCallFailed { status } => Ok(StatusMessage {
     128            6 :                 message: "gRPC call failed".to_string(),
     129            6 :                 details: HashMap::from_iter([
     130            6 :                     ("code", status.code().to_string()),
     131            6 :                     ("error", status.to_string()),
     132            6 :                 ]),
     133            6 :             }),
     134            4 :             Status::GrpcStreamReadFailed {  channel_id, stream_id, element, status  } => Ok(StatusMessage {
     135            4 :                 message: "reading from gRPC stream failed".to_string(),
     136            4 :                 details: HashMap::from_iter([
     137            4 :                     ("target", match element { GrpcStreamElement::Trailers => "trailers", GrpcStreamElement::Message => "message" }.to_string()),
     138            4 :                     ("stream", stream_id.to_string()),
     139            4 :                     ("channel", channel_id.to_string()),
     140            4 :                     ("code", status.code().to_string()),
     141            4 :                     ("error", status.to_string()),
     142              :                 ]),
     143              :             }),
     144            2 :             Status::GrpcStreamReadTimedOut { channel_id, stream_id } => Ok(StatusMessage {
     145            2 :                 message: "reading from gRPC stream timed out".to_string(),
     146            2 :                 details: HashMap::from_iter([
     147            2 :                     ("stream", stream_id.to_string()),
     148            2 :                     ("channel", channel_id.to_string()),
     149            2 :                 ]),
     150            2 :             }),
     151            4 :             Status::GrpcStreamIsUnknown { channel_id, stream_id } => Ok(StatusMessage {
     152            4 :                 message: "gRPC stream is unknown".to_string(),
     153            4 :                 details: HashMap::from_iter([
     154            4 :                     ("stream", stream_id.to_string()),
     155            4 :                     ("channel", channel_id.to_string()),
     156            4 :                 ]),
     157            4 :             }),
     158            1 :             Status::GrpcStreamClosed { channel_id, stream_id } => Ok(StatusMessage {
     159            1 :                 message: "gRPC stream is closed".to_string(),
     160            1 :                 details: HashMap::from_iter([
     161            1 :                     ("stream", stream_id.to_string()),
     162            1 :                     ("channel", channel_id.to_string()),
     163            1 :                 ]),
     164            1 :             }),
     165            2 :             Status::HttpRequestFailed { stream_id, error } => Ok(StatusMessage {
     166            2 :                 message: "http request failed".to_string(),
     167            2 :                 details: HashMap::from_iter([
     168            2 :                     ("stream", stream_id.to_string()),
     169            2 :                     ("error", error.to_string()),
     170            2 :                 ]),
     171            2 :             }),
     172            2 :             Status::HttpStreamReadFailed { stream_id, error } => Ok(StatusMessage {
     173            2 :                 message: "reading chunk from http stream failed".to_string(),
     174            2 :                 details: HashMap::from_iter([
     175            2 :                     ("stream", stream_id.to_string()),
     176            2 :                     ("error", error.to_string()),
     177            2 :                 ]),
     178            2 :             }),
     179            2 :             Status::HttpStreamReadTimedOut { stream_id } => Ok(StatusMessage {
     180            2 :                 message: "reading from http stream timed out".to_string(),
     181            2 :                 details: HashMap::from_iter([
     182            2 :                     ("stream", stream_id.to_string()),
     183            2 :                 ]),
     184            2 :             }),
     185            5 :             Status::HttpStreamIsUnknown { stream_id } => Ok(StatusMessage {
     186            5 :                 message: "http stream is unknown".to_string(),
     187            5 :                 details: HashMap::from_iter([
     188            5 :                     ("stream", stream_id.to_string()),
     189            5 :                 ]),
     190            5 :             }),
     191            1 :             Status::HttpStreamClosed { stream_id } => Ok(StatusMessage {
     192            1 :                 message: "http stream is closed".to_string(),
     193            1 :                 details: HashMap::from_iter([
     194            1 :                     ("stream", stream_id.to_string()),
     195            1 :                 ]),
     196            1 :             }),
     197            1 :             Status::HttpStreamFailed { stream_id, error } => Ok(StatusMessage {
     198            1 :                 message: "http stream failed".to_string(),
     199            1 :                 details: HashMap::from_iter([
     200            1 :                     ("stream", stream_id.to_string()),
     201            1 :                     ("error", error.to_string()),
     202            1 :                 ]),
     203            1 :             }),
     204            1 :             Status::HttpClientConfigInvalid {  message } => Ok(StatusMessage {
     205            1 :                 message: format!("http client config is invalid"),
     206            1 :                 details: HashMap::from_iter([
     207            1 :                     ("error", message.to_string()),
     208            1 :                 ]),
     209            1 :             }),
     210            1 :             Status::HttpUrlIsInvalid { message } => Ok(StatusMessage {
     211            1 :                 message: format!("http url is invalid"),
     212            1 :                 details: HashMap::from_iter([
     213            1 :                     ("error", message.to_string()),
     214            1 :                 ]),
     215            1 :             }),
     216              :         }
     217           71 :     }
     218              : }
     219              : 
     220              : impl From<&Status> for StatusCode {
     221           71 :     fn from(status: &Status) -> StatusCode {
     222           71 :         match status {
     223            6 :             Status::GrpcCallFailed { status: _ } => StatusCode::BAD_REQUEST,
     224            5 :             Status::GrpcChannelIdIsUnknown { channel_id: _ } => StatusCode::NOT_FOUND,
     225            3 :             Status::GrpcEndpointConnectFailed { message: _ } => StatusCode::BAD_REQUEST,
     226            3 :             Status::GrpcTlsConfigInvalid { message: _ } => StatusCode::BAD_REQUEST,
     227            1 :             Status::GrpcStreamClosed { channel_id: _, stream_id: _ } => StatusCode::BAD_REQUEST,
     228            4 :             Status::GrpcStreamIsUnknown { channel_id: _, stream_id: _ } => StatusCode::NOT_FOUND,
     229            4 :             Status::GrpcStreamReadFailed { channel_id: _, stream_id: _, element: _, status: _ } => StatusCode::BAD_REQUEST,
     230            2 :             Status::GrpcStreamReadTimedOut { channel_id: _, stream_id: _ } => StatusCode::BAD_REQUEST,
     231            1 :             Status::HeaderHasInvalidEncoding { header: _, message: _ } => StatusCode::BAD_REQUEST,
     232            2 :             Status::HeaderIsNotAValidEndpoint { header: _, message: _ } => StatusCode::BAD_REQUEST,
     233            3 :             Status::HeaderIsNotAnUsize { header: _, message: _ } => StatusCode::BAD_REQUEST,
     234            3 :             Status::HeaderPathIsInvalid { header: _, path: _, message: _ } => StatusCode::BAD_REQUEST,
     235           15 :             Status::HeaderRequiredButMissing { header: _ } => StatusCode::BAD_REQUEST,
     236            1 :             Status::HttpClientConfigInvalid { message: _ } => StatusCode::BAD_REQUEST,
     237            2 :             Status::HttpEndpointIsInvalid { header: _, endpoint: _ , message: _ } => StatusCode::BAD_REQUEST,
     238            2 :             Status::HttpMethodIsInvalid { header: _, method: _, message: _ } => StatusCode::BAD_REQUEST,
     239            2 :             Status::HttpRequestFailed { stream_id: _, error: _ } => StatusCode::BAD_REQUEST,
     240            1 :             Status::HttpStreamClosed { stream_id: _ } => StatusCode::BAD_REQUEST,
     241            1 :             Status::HttpStreamFailed { stream_id: _, error: _ } => StatusCode::BAD_REQUEST,
     242            5 :             Status::HttpStreamIsUnknown { stream_id: _ } => StatusCode::NOT_FOUND,
     243            2 :             Status::HttpStreamReadFailed { stream_id: _, error: _ } => StatusCode::BAD_REQUEST,
     244            2 :             Status::HttpStreamReadTimedOut { stream_id: _ } => StatusCode::BAD_REQUEST,
     245            1 :             Status::HttpUrlIsInvalid { message: _ } => StatusCode::BAD_REQUEST,
     246              :         }
     247           71 :     }
     248              : }
     249              : 
     250              : /// Percent-encode a gRPC status message for a header, the encoding of the `grpc-message` trailer.
     251              : /// Database errors span lines and quote the text of the user, which a header value cannot hold.
     252           10 : fn encode_grpc_message(message: &str) -> String {
     253           10 :     let mut encoded = String::with_capacity(message.len());
     254          162 :     for byte in message.bytes() {
     255          162 :         if (0x20..=0x7E).contains(&byte) && byte != b'%' {
     256          150 :             encoded.push(byte as char);
     257          150 :         } else {
     258           12 :             encoded.push_str(&format!("%{byte:02X}"));
     259           12 :         }
     260              :     }
     261           10 :     encoded
     262           10 : }
     263              : 
     264              : /// Build an error response for a failed gRPC call/stream, forwarding the
     265              : /// gRPC "richer error model" data to the frontend:
     266              : /// - `dashql-grpc-status`: the numeric gRPC status code
     267              : /// - `dashql-grpc-message`: the terse gRPC status message, percent-encoded
     268              : /// - `dashql-grpc-status-details-bin`: base64 of the `google.rpc.Status`
     269              : ///   details envelope (carrying Hyper's `ErrorInfo`), if present.
     270           10 : fn grpc_error_response(s: &Status, status: &tonic::Status, body: Vec<u8>) -> Response<Vec<u8>> {
     271           10 :     let grpc_code = (status.code() as usize).to_string();
     272           10 :     let mut builder = Response::builder()
     273           10 :         .status(StatusCode::from(s).as_u16())
     274           10 :         .header(HEADER_NAME_ERROR, "true")
     275           10 :         .header(HEADER_NAME_GRPC_STATUS, &grpc_code)
     276           10 :         .header(HEADER_NAME_GRPC_MESSAGE, encode_grpc_message(status.message()))
     277           10 :         .header(CONTENT_TYPE, mime::APPLICATION_JSON.essence_str());
     278              :     // Forward the raw status-details-bin (google.rpc.Status) as base64, if any.
     279              :     // The frontend decodes it to surface Hyper's ErrorInfo (sqlstate, hint,
     280              :     // customer/system detail, SQL text position, ...).
     281           10 :     let details = status.details();
     282           10 :     if !details.is_empty() {
     283            3 :         let encoded = base64::engine::general_purpose::STANDARD.encode(details);
     284            3 :         builder = builder.header(HEADER_NAME_GRPC_STATUS_DETAILS, encoded);
     285            7 :     }
     286           10 :     builder.body(body).unwrap()
     287           10 : }
     288              : 
     289              : impl From<&Status> for Response<Vec<u8>> {
     290           71 :     fn from(s: &Status) -> Response<Vec<u8>> {
     291           71 :         let mut body: Vec<u8> = Vec::new();
     292           71 :         if let Ok(status_msg) = StatusMessage::try_from(s) {
     293           71 :             body = serde_json::to_vec(&status_msg).unwrap_or_default();
     294           71 :         }
     295           71 :         match &s {
     296            6 :             Status::GrpcCallFailed { ref status }
     297            4 :             | Status::GrpcStreamReadFailed { channel_id: _, stream_id: _, element: _, ref status } => {
     298           10 :                 grpc_error_response(s, status, body)
     299              :             },
     300              :             _ => {
     301           61 :                 Response::builder()
     302           61 :                     .status(StatusCode::from(s).as_u16())
     303           61 :                     .header(HEADER_NAME_ERROR, "true")
     304           61 :                     .header(CONTENT_TYPE, mime::APPLICATION_JSON.essence_str())
     305           61 :                     .body(body)
     306           61 :                     .unwrap()
     307              :             }
     308              :         }
     309           71 :     }
     310              : }
     311              : 
     312              : #[cfg(test)]
     313              : mod test {
     314              :     use super::*;
     315              : 
     316           57 :     fn header(response: &Response<Vec<u8>>, name: &str) -> Option<String> {
     317           57 :         response.headers().get(name).map(|value| value.to_str().unwrap().to_string())
     318           57 :     }
     319              : 
     320           26 :     fn json(response: &Response<Vec<u8>>) -> serde_json::Value {
     321           26 :         serde_json::from_slice(response.body()).unwrap()
     322           26 :     }
     323              : 
     324              :     #[test]
     325            1 :     fn every_status_describes_itself_as_json() {
     326           13 :         let text = || "text".to_string();
     327            3 :         let grpc = || tonic::Status::internal("boom");
     328            1 :         let cases: Vec<(Status, u16, &str, &[(&str, &str)])> = vec![
     329            1 :             (Status::GrpcChannelIdIsUnknown { channel_id: 1 }, 404, "gRPC channel id is unknown", &[("channel", "1")]),
     330            1 :             (Status::GrpcEndpointConnectFailed { message: text() }, 400, "connecting to gRPC endpoint failed", &[("error", "text")]),
     331            1 :             (Status::GrpcTlsConfigInvalid { message: text() }, 400, "gRPC tls config is invalid", &[("error", "text")]),
     332            1 :             (Status::GrpcStreamClosed { channel_id: 1, stream_id: 2 }, 400, "gRPC stream is closed", &[("channel", "1"), ("stream", "2")]),
     333            1 :             (Status::GrpcStreamIsUnknown { channel_id: 1, stream_id: 2 }, 404, "gRPC stream is unknown", &[("channel", "1"), ("stream", "2")]),
     334            1 :             (Status::GrpcStreamReadTimedOut { channel_id: 1, stream_id: 2 }, 400, "reading from gRPC stream timed out", &[("channel", "1"), ("stream", "2")]),
     335            1 :             (Status::GrpcCallFailed { status: grpc() }, 400, "gRPC call failed", &[]),
     336            1 :             (Status::GrpcStreamReadFailed { channel_id: 1, stream_id: 2, element: GrpcStreamElement::Message, status: grpc() }, 400, "reading from gRPC stream failed", &[("target", "message")]),
     337            1 :             (Status::GrpcStreamReadFailed { channel_id: 1, stream_id: 2, element: GrpcStreamElement::Trailers, status: grpc() }, 400, "reading from gRPC stream failed", &[("target", "trailers")]),
     338            1 :             (Status::HeaderHasInvalidEncoding { header: "h", message: text() }, 400, "header has an invalid encoding", &[("header", "h"), ("error", "text")]),
     339            1 :             (Status::HeaderIsNotAValidEndpoint { header: "h", message: text() }, 400, "header is not a valid endpoint", &[("header", "h"), ("error", "text")]),
     340            1 :             (Status::HeaderIsNotAnUsize { header: "h", message: text() }, 400, "header is not an unsigned integer", &[("header", "h"), ("error", "text")]),
     341            1 :             (Status::HeaderPathIsInvalid { header: "h", path: "p".to_string(), message: text() }, 400, "header stores invalid path", &[("header", "h"), ("path", "p")]),
     342            1 :             (Status::HeaderRequiredButMissing { header: "h" }, 400, "header is missing", &[("header", "h")]),
     343            1 :             (Status::HttpClientConfigInvalid { message: text() }, 400, "http client config is invalid", &[("error", "text")]),
     344            1 :             (Status::HttpEndpointIsInvalid { header: "h", endpoint: "e".to_string(), message: text() }, 400, "header stores invalid endpoint", &[("endpoint", "e")]),
     345            1 :             (Status::HttpMethodIsInvalid { header: "h", method: "m".to_string(), message: text() }, 400, "header stores invalid method", &[("method", "m")]),
     346            1 :             (Status::HttpRequestFailed { stream_id: 2, error: text() }, 400, "http request failed", &[("stream", "2"), ("error", "text")]),
     347            1 :             (Status::HttpStreamClosed { stream_id: 2 }, 400, "http stream is closed", &[("stream", "2")]),
     348            1 :             (Status::HttpStreamFailed { stream_id: 2, error: text() }, 400, "http stream failed", &[("stream", "2"), ("error", "text")]),
     349            1 :             (Status::HttpStreamIsUnknown { stream_id: 2 }, 404, "http stream is unknown", &[("stream", "2")]),
     350            1 :             (Status::HttpStreamReadFailed { stream_id: 2, error: text() }, 400, "reading chunk from http stream failed", &[("stream", "2"), ("error", "text")]),
     351            1 :             (Status::HttpStreamReadTimedOut { stream_id: 2 }, 400, "reading from http stream timed out", &[("stream", "2")]),
     352            1 :             (Status::HttpUrlIsInvalid { message: text() }, 400, "http url is invalid", &[("error", "text")]),
     353              :         ];
     354           24 :         for (status, code, message, details) in cases {
     355           24 :             let response = Response::from(&status);
     356           24 :             assert_eq!(response.status().as_u16(), code, "{status:?}");
     357           24 :             assert_eq!(header(&response, HEADER_NAME_ERROR).as_deref(), Some("true"), "{status:?}");
     358           24 :             assert_eq!(header(&response, "content-type").as_deref(), Some("application/json"), "{status:?}");
     359           24 :             let body = json(&response);
     360           24 :             assert_eq!(body["message"], message, "{status:?}");
     361           33 :             for (name, value) in details {
     362           33 :                 assert_eq!(body["details"][name], *value, "{status:?}");
     363              :             }
     364              :         }
     365            1 :     }
     366              : 
     367              :     #[test]
     368            1 :     fn a_failed_grpc_call_forwards_the_rich_error() {
     369            1 :         let status = tonic::Status::with_details(tonic::Code::InvalidArgument, "bad query", vec![0, 1, 255].into());
     370            1 :         let response = Response::from(&Status::GrpcCallFailed { status });
     371            1 :         assert_eq!(header(&response, HEADER_NAME_GRPC_STATUS).as_deref(), Some("3"));
     372            1 :         assert_eq!(header(&response, HEADER_NAME_GRPC_MESSAGE).as_deref(), Some("bad query"));
     373            1 :         assert_eq!(header(&response, HEADER_NAME_GRPC_STATUS_DETAILS).as_deref(), Some("AAH/"));
     374            1 :     }
     375              : 
     376              :     #[test]
     377            1 :     fn a_failed_grpc_call_without_details_sends_none() {
     378            1 :         let response = Response::from(&Status::GrpcCallFailed { status: tonic::Status::internal("boom") });
     379            1 :         assert_eq!(header(&response, HEADER_NAME_GRPC_STATUS).as_deref(), Some("13"));
     380            1 :         assert_eq!(header(&response, HEADER_NAME_GRPC_STATUS_DETAILS), None);
     381            1 :     }
     382              : 
     383              :     #[test]
     384            1 :     fn a_grpc_message_that_is_no_header_value_reaches_the_app() {
     385              :         // Database errors span lines and quote the text of the user
     386            1 :         let message = "syntax error at line 1\nnear \"caf\u{e9}\" 100%";
     387            1 :         let status = tonic::Status::new(tonic::Code::InvalidArgument, message);
     388            2 :         for status in [
     389            1 :             Status::GrpcCallFailed { status: status.clone() },
     390            1 :             Status::GrpcStreamReadFailed { channel_id: 1, stream_id: 2, element: GrpcStreamElement::Message, status },
     391            1 :         ] {
     392            2 :             let response = Response::from(&status);
     393            2 :             assert_eq!(header(&response, HEADER_NAME_GRPC_STATUS).as_deref(), Some("3"));
     394              :             // Percent-encoded like the `grpc-message` trailer it comes from
     395            2 :             assert_eq!(
     396            2 :                 header(&response, HEADER_NAME_GRPC_MESSAGE).as_deref(),
     397              :                 Some("syntax error at line 1%0Anear \"caf%C3%A9\" 100%25"),
     398              :             );
     399            2 :             assert!(json(&response)["details"]["error"].as_str().unwrap().contains("caf\u{e9}"));
     400              :         }
     401            1 :     }
     402              : }
        

Generated by: LCOV version 2.0-1