From a52de8aec35acc978d6c0721ed6589088e412f10 Mon Sep 17 00:00:00 2001 From: Rafael Lemos Date: Fri, 17 Feb 2023 12:18:46 -0300 Subject: [PATCH] types: add support for `RequestInfo` error message type (#1280) Following implementation at flemosr/tonic-richer-error. --- tonic-types/src/lib.rs | 3 +- .../src/richer_error/error_details/mod.rs | 54 ++++++++- .../src/richer_error/error_details/vec.rs | 11 +- tonic-types/src/richer_error/mod.rs | 57 ++++++++- .../src/richer_error/std_messages/mod.rs | 4 + .../richer_error/std_messages/request_info.rs | 112 ++++++++++++++++++ 6 files changed, 235 insertions(+), 6 deletions(-) create mode 100644 tonic-types/src/richer_error/std_messages/request_info.rs diff --git a/tonic-types/src/lib.rs b/tonic-types/src/lib.rs index 6016dd8..e59b521 100644 --- a/tonic-types/src/lib.rs +++ b/tonic-types/src/lib.rs @@ -47,7 +47,8 @@ mod richer_error; pub use richer_error::{ BadRequest, DebugInfo, ErrorDetail, ErrorDetails, ErrorInfo, FieldViolation, - PreconditionFailure, PreconditionViolation, QuotaFailure, QuotaViolation, RetryInfo, StatusExt, + PreconditionFailure, PreconditionViolation, QuotaFailure, QuotaViolation, RequestInfo, + RetryInfo, StatusExt, }; mod sealed { diff --git a/tonic-types/src/richer_error/error_details/mod.rs b/tonic-types/src/richer_error/error_details/mod.rs index 843bd34..f559820 100644 --- a/tonic-types/src/richer_error/error_details/mod.rs +++ b/tonic-types/src/richer_error/error_details/mod.rs @@ -2,7 +2,7 @@ use std::{collections::HashMap, time}; use super::std_messages::{ BadRequest, DebugInfo, ErrorInfo, FieldViolation, PreconditionFailure, PreconditionViolation, - QuotaFailure, QuotaViolation, RetryInfo, + QuotaFailure, QuotaViolation, RequestInfo, RetryInfo, }; pub(crate) mod vec; @@ -31,6 +31,9 @@ pub struct ErrorDetails { /// This field stores [`BadRequest`] data, if any. pub(crate) bad_request: Option, + + /// This field stores [`RequestInfo`] data, if any. + pub(crate) request_info: Option, } impl ErrorDetails { @@ -250,6 +253,29 @@ impl ErrorDetails { } } + /// Generates an [`ErrorDetails`] struct with [`RequestInfo`] details and + /// remaining fields set to `None`. + /// + /// # Examples + /// + /// ``` + /// use tonic_types::ErrorDetails; + /// + /// let err_details = ErrorDetails::with_request_info( + /// "request_id", + /// "serving_data", + /// ); + /// ``` + pub fn with_request_info( + request_id: impl Into, + serving_data: impl Into, + ) -> Self { + ErrorDetails { + request_info: Some(RequestInfo::new(request_id, serving_data)), + ..ErrorDetails::new() + } + } + /// Get [`RetryInfo`] details, if any. pub fn retry_info(&self) -> Option { self.retry_info.clone() @@ -280,6 +306,11 @@ impl ErrorDetails { self.bad_request.clone() } + /// Get [`RequestInfo`] details, if any. + pub fn request_info(&self) -> Option { + self.request_info.clone() + } + /// Set [`RetryInfo`] details. Can be chained with other `.set_` and /// `.add_` [`ErrorDetails`] methods. /// @@ -586,4 +617,25 @@ impl ErrorDetails { } false } + + /// Set [`RequestInfo`] details. Can be chained with other `.set_` and + /// `.add_` [`ErrorDetails`] methods. + /// + /// # Examples + /// + /// ``` + /// use tonic_types::ErrorDetails; + /// + /// let mut err_details = ErrorDetails::new(); + /// + /// err_details.set_request_info("request_id", "serving_data"); + /// ``` + pub fn set_request_info( + &mut self, + request_id: impl Into, + serving_data: impl Into, + ) -> &mut Self { + self.request_info = Some(RequestInfo::new(request_id, serving_data)); + self + } } diff --git a/tonic-types/src/richer_error/error_details/vec.rs b/tonic-types/src/richer_error/error_details/vec.rs index 4393515..c7e5161 100644 --- a/tonic-types/src/richer_error/error_details/vec.rs +++ b/tonic-types/src/richer_error/error_details/vec.rs @@ -1,5 +1,5 @@ use super::super::std_messages::{ - BadRequest, DebugInfo, ErrorInfo, PreconditionFailure, QuotaFailure, RetryInfo, + BadRequest, DebugInfo, ErrorInfo, PreconditionFailure, QuotaFailure, RequestInfo, RetryInfo, }; /// Wraps the structs corresponding to the standard error messages, allowing @@ -24,6 +24,9 @@ pub enum ErrorDetail { /// Wraps the [`BadRequest`] struct. BadRequest(BadRequest), + + /// Wraps the [`RequestInfo`] struct. + RequestInfo(RequestInfo), } impl From for ErrorDetail { @@ -61,3 +64,9 @@ impl From for ErrorDetail { ErrorDetail::BadRequest(err_detail) } } + +impl From for ErrorDetail { + fn from(err_detail: RequestInfo) -> Self { + ErrorDetail::RequestInfo(err_detail) + } +} diff --git a/tonic-types/src/richer_error/mod.rs b/tonic-types/src/richer_error/mod.rs index 906dea4..7b6b75e 100644 --- a/tonic-types/src/richer_error/mod.rs +++ b/tonic-types/src/richer_error/mod.rs @@ -13,7 +13,7 @@ use super::pb; pub use error_details::{vec::ErrorDetail, ErrorDetails}; pub use std_messages::{ BadRequest, DebugInfo, ErrorInfo, FieldViolation, PreconditionFailure, PreconditionViolation, - QuotaFailure, QuotaViolation, RetryInfo, + QuotaFailure, QuotaViolation, RequestInfo, RetryInfo, }; trait IntoAny { @@ -381,6 +381,28 @@ pub trait StatusExt: crate::sealed::Sealed { /// } /// ``` fn get_details_bad_request(&self) -> Option; + + /// Get first [`RequestInfo`] details found on `tonic::Status`, if any. + /// If some `prost::DecodeError` occurs, returns `None`. + /// + /// # Examples + /// + /// ``` + /// use tonic::{Status, Response}; + /// use tonic_types::StatusExt; + /// + /// fn handle_request_result(req_result: Result, Status>) { + /// match req_result { + /// Ok(_) => {}, + /// Err(status) => { + /// if let Some(request_info) = status.get_details_request_info() { + /// // Handle request_info details + /// } + /// } + /// }; + /// } + /// ``` + fn get_details_request_info(&self) -> Option; } impl crate::sealed::Sealed for tonic::Status {} @@ -420,6 +442,10 @@ impl StatusExt for tonic::Status { conv_details.push(bad_request.into_any()); } + if let Some(request_info) = details.request_info { + conv_details.push(request_info.into_any()); + } + let details = gen_details_bytes(code, &message, conv_details); tonic::Status::with_details_and_metadata(code, message, details, metadata) @@ -459,6 +485,9 @@ impl StatusExt for tonic::Status { ErrorDetail::BadRequest(bad_req) => { conv_details.push(bad_req.into_any()); } + ErrorDetail::RequestInfo(req_info) => { + conv_details.push(req_info.into_any()); + } } } @@ -505,6 +534,9 @@ impl StatusExt for tonic::Status { BadRequest::TYPE_URL => { details.bad_request = Some(BadRequest::from_any(any)?); } + RequestInfo::TYPE_URL => { + details.request_info = Some(RequestInfo::from_any(any)?); + } _ => {} } } @@ -541,6 +573,9 @@ impl StatusExt for tonic::Status { BadRequest::TYPE_URL => { details.push(BadRequest::from_any(any)?.into()); } + RequestInfo::TYPE_URL => { + details.push(RequestInfo::from_any(any)?.into()); + } _ => {} } } @@ -635,6 +670,20 @@ impl StatusExt for tonic::Status { None } + + fn get_details_request_info(&self) -> Option { + let status = pb::Status::decode(self.details()).ok()?; + + for any in status.details.into_iter() { + if any.type_url.as_str() == RequestInfo::TYPE_URL { + if let Ok(detail) = RequestInfo::from_any(any) { + return Some(detail); + } + } + } + + None + } } #[cfg(test)] @@ -644,7 +693,7 @@ mod tests { use super::{ BadRequest, DebugInfo, ErrorDetails, ErrorInfo, PreconditionFailure, QuotaFailure, - RetryInfo, StatusExt, + RequestInfo, RetryInfo, StatusExt, }; #[test] @@ -663,7 +712,8 @@ mod tests { .add_quota_failure_violation("clientip:", "description") .set_error_info("SOME_INFO", "example.local", metadata.clone()) .add_precondition_failure_violation("TOS", "example.local", "description") - .add_bad_request_violation("field", "description"); + .add_bad_request_violation("field", "description") + .set_request_info("request-id", "some-request-data"); let fmt_details = format!("{:?}", err_details); @@ -678,6 +728,7 @@ mod tests { ErrorInfo::new("SOME_INFO", "example.local", metadata).into(), PreconditionFailure::with_violation("TOS", "example.local", "description").into(), BadRequest::with_violation("field", "description").into(), + RequestInfo::new("request-id", "some-request-data").into(), ]; let fmt_details_vec = format!("{:?}", err_details_vec); diff --git a/tonic-types/src/richer_error/std_messages/mod.rs b/tonic-types/src/richer_error/std_messages/mod.rs index 799aaca..3ff6c11 100644 --- a/tonic-types/src/richer_error/std_messages/mod.rs +++ b/tonic-types/src/richer_error/std_messages/mod.rs @@ -21,3 +21,7 @@ pub use prec_failure::{PreconditionFailure, PreconditionViolation}; mod bad_request; pub use bad_request::{BadRequest, FieldViolation}; + +mod request_info; + +pub use request_info::RequestInfo; diff --git a/tonic-types/src/richer_error/std_messages/request_info.rs b/tonic-types/src/richer_error/std_messages/request_info.rs new file mode 100644 index 0000000..d7ec249 --- /dev/null +++ b/tonic-types/src/richer_error/std_messages/request_info.rs @@ -0,0 +1,112 @@ +use prost::{DecodeError, Message}; +use prost_types::Any; + +use super::super::{pb, FromAny, IntoAny}; + +/// Used to encode/decode the `RequestInfo` standard error message described +/// in [error_details.proto]. Contains metadata about the request that +/// clients can attach when providing feedback. +/// +/// [error_details.proto]: https://github.com/googleapis/googleapis/blob/master/google/rpc/error_details.proto +#[derive(Clone, Debug)] +pub struct RequestInfo { + /// An opaque string that should only be interpreted by the service that + /// generated it. For example, an id used to identify requests in the logs. + pub request_id: String, + + /// Any data used to serve this request. For example, an encrypted stack + /// trace that can be sent back to the service provider for debugging. + pub serving_data: String, +} + +impl RequestInfo { + /// Type URL of the `RequestInfo` standard error message type. + pub const TYPE_URL: &'static str = "type.googleapis.com/google.rpc.RequestInfo"; + + /// Creates a new [`RequestInfo`] struct. + pub fn new(request_id: impl Into, serving_data: impl Into) -> Self { + RequestInfo { + request_id: request_id.into(), + serving_data: serving_data.into(), + } + } + + /// Returns `true` if [`RequestInfo`] fields are empty, and `false` if they + /// are not. + pub fn is_empty(&self) -> bool { + self.request_id.is_empty() && self.serving_data.is_empty() + } +} + +impl IntoAny for RequestInfo { + fn into_any(self) -> Any { + let detail_data = pb::RequestInfo { + request_id: self.request_id, + serving_data: self.serving_data, + }; + + Any { + type_url: RequestInfo::TYPE_URL.to_string(), + value: detail_data.encode_to_vec(), + } + } +} + +impl FromAny for RequestInfo { + fn from_any(any: Any) -> Result { + let buf: &[u8] = &any.value; + let req_info = pb::RequestInfo::decode(buf)?; + + let debug_info = RequestInfo { + request_id: req_info.request_id, + serving_data: req_info.serving_data, + }; + + Ok(debug_info) + } +} + +#[cfg(test)] +mod tests { + use super::super::super::{FromAny, IntoAny}; + use super::RequestInfo; + + #[test] + fn gen_error_info() { + let error_info = RequestInfo::new("some-id", "some-data"); + + let formatted = format!("{:?}", error_info); + + let expected_filled = + "RequestInfo { request_id: \"some-id\", serving_data: \"some-data\" }"; + + assert!( + formatted.eq(expected_filled), + "filled RequestInfo differs from expected result" + ); + + let gen_any = error_info.into_any(); + + let formatted = format!("{:?}", gen_any); + + let expected = + "Any { type_url: \"type.googleapis.com/google.rpc.RequestInfo\", value: [10, 7, 115, 111, 109, 101, 45, 105, 100, 18, 9, 115, 111, 109, 101, 45, 100, 97, 116, 97] }"; + + assert!( + formatted.eq(expected), + "Any from filled RequestInfo differs from expected result" + ); + + let br_details = match RequestInfo::from_any(gen_any) { + Err(error) => panic!("Error generating RequestInfo from Any: {:?}", error), + Ok(from_any) => from_any, + }; + + let formatted = format!("{:?}", br_details); + + assert!( + formatted.eq(expected_filled), + "RequestInfo from Any differs from expected result" + ); + } +}