diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 3868925c3..2a378ff4e 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -233,6 +233,26 @@ name = "json-codec-server" path = "src/json-codec/server.rs" required-features = ["json-codec"] +[[bin]] +name = "richer-error-client" +path = "src/richer-error/client.rs" +required-features = ["types"] + +[[bin]] +name = "richer-error-server" +path = "src/richer-error/server.rs" +required-features = ["types"] + +[[bin]] +name = "richer-error-client-vec" +path = "src/richer-error/client_vec.rs" +required-features = ["types"] + +[[bin]] +name = "richer-error-server-vec" +path = "src/richer-error/server_vec.rs" +required-features = ["types"] + [features] gcp = ["dep:prost-types", "tonic/tls"] routeguide = ["dep:async-stream", "dep:futures", "tokio-stream", "dep:rand", "dep:serde", "dep:serde_json"] @@ -254,8 +274,9 @@ tls-rustls = ["dep:hyper", "dep:hyper-rustls", "dep:tower", "tower-http/util", " dynamic-load-balance = ["dep:tower"] timeout = ["tokio/time", "dep:tower"] tls-client-auth = ["tonic/tls"] +types = ["dep:tonic-types"] -full = ["gcp", "routeguide", "reflection", "autoreload", "health", "grpc-web", "tracing", "hyper-warp", "hyper-warp-multiplex", "uds", "streaming", "mock", "tower", "json-codec", "compression", "tls", "tls-rustls", "dynamic-load-balance", "timeout", "tls-client-auth"] +full = ["gcp", "routeguide", "reflection", "autoreload", "health", "grpc-web", "tracing", "hyper-warp", "hyper-warp-multiplex", "uds", "streaming", "mock", "tower", "json-codec", "compression", "tls", "tls-rustls", "dynamic-load-balance", "timeout", "tls-client-auth", "types"] default = ["full"] [dependencies] @@ -267,6 +288,7 @@ tonic = { path = "../tonic" } tonic-web = { path = "../tonic-web", optional = true } tonic-health = { path = "../tonic-health", optional = true } tonic-reflection = { path = "../tonic-reflection", optional = true } +tonic-types = { path = "../tonic-types", optional = true } async-stream = { version = "0.3", optional = true } futures = { version = "0.3", default-features = false, optional = true } tokio-stream = { version = "0.1", optional = true } diff --git a/examples/README.md b/examples/README.md index f11da34b1..ffa350af5 100644 --- a/examples/README.md +++ b/examples/README.md @@ -153,3 +153,33 @@ The autoload example requires the following crates installed globally: * [systemfd](https://crates.io/crates/systemfd) * [cargo-watch](https://crates.io/crates/cargo-watch) + +## Richer Error + +Both clients and both servers do the same thing, but using the two different +approaches. Run one of the servers in one terminal, and then run the clients +in another. + +### Client using the `ErrorDetails` struct + +```bash +$ cargo run --bin richer-error-client +``` + +### Client using a vector of error message types + +```bash +$ cargo run --bin richer-error-client-vec +``` + +### Server using the `ErrorDetails` struct + +```bash +$ cargo run --bin richer-error-server +``` + +### Server using a vector of error message types + +```bash +$ cargo run --bin richer-error-server-vec +``` \ No newline at end of file diff --git a/examples/src/richer-error/client.rs b/examples/src/richer-error/client.rs new file mode 100644 index 000000000..1fa9ef7e2 --- /dev/null +++ b/examples/src/richer-error/client.rs @@ -0,0 +1,52 @@ +use tonic_types::StatusExt; + +use hello_world::greeter_client::GreeterClient; +use hello_world::HelloRequest; + +pub mod hello_world { + tonic::include_proto!("helloworld"); +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let mut client = GreeterClient::connect("http://[::1]:50051").await?; + + let request = tonic::Request::new(HelloRequest { + // Valid request + // name: "Tonic".into(), + // Name cannot be empty + name: "".into(), + // Name is too long + // name: "some excessively long name".into(), + }); + + let response = match client.say_hello(request).await { + Ok(response) => response, + Err(status) => { + println!(" Error status received. Extracting error details...\n"); + + let err_details = status.get_error_details(); + + if let Some(bad_request) = err_details.bad_request() { + // Handle bad_request details + println!(" {:?}", bad_request); + } + if let Some(help) = err_details.help() { + // Handle help details + println!(" {:?}", help); + } + if let Some(localized_message) = err_details.localized_message() { + // Handle localized_message details + println!(" {:?}", localized_message); + } + + println!(); + + return Ok(()); + } + }; + + println!(" Successfull response received.\n\n {:?}\n", response); + + Ok(()) +} diff --git a/examples/src/richer-error/client_vec.rs b/examples/src/richer-error/client_vec.rs new file mode 100644 index 000000000..f74aa9f31 --- /dev/null +++ b/examples/src/richer-error/client_vec.rs @@ -0,0 +1,58 @@ +use tonic_types::{ErrorDetail, StatusExt}; + +use hello_world::greeter_client::GreeterClient; +use hello_world::HelloRequest; + +pub mod hello_world { + tonic::include_proto!("helloworld"); +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let mut client = GreeterClient::connect("http://[::1]:50051").await?; + + let request = tonic::Request::new(HelloRequest { + // Valid request + // name: "Tonic".into(), + // Name cannot be empty + name: "".into(), + // Name is too long + // name: "some excessively long name".into(), + }); + + let response = match client.say_hello(request).await { + Ok(response) => response, + Err(status) => { + println!(" Error status received. Extracting error details...\n"); + + let err_details = status.get_error_details_vec(); + + for (i, err_detail) in err_details.iter().enumerate() { + println!("err_detail[{i}]"); + match err_detail { + ErrorDetail::BadRequest(bad_request) => { + // Handle bad_request details + println!(" {:?}", bad_request); + } + ErrorDetail::Help(help) => { + // Handle help details + println!(" {:?}", help); + } + ErrorDetail::LocalizedMessage(localized_message) => { + // Handle localized_message details + println!(" {:?}", localized_message); + } + _ => {} + } + } + + println!(); + + return Ok(()); + } + }; + + println!(" Successfull response received.\n\n {:?}\n", response); + + Ok(()) +} diff --git a/examples/src/richer-error/server.rs b/examples/src/richer-error/server.rs new file mode 100644 index 000000000..cb1e6cf38 --- /dev/null +++ b/examples/src/richer-error/server.rs @@ -0,0 +1,71 @@ +use tonic::{transport::Server, Code, Request, Response, Status}; +use tonic_types::{ErrorDetails, StatusExt}; + +use hello_world::greeter_server::{Greeter, GreeterServer}; +use hello_world::{HelloReply, HelloRequest}; + +pub mod hello_world { + tonic::include_proto!("helloworld"); +} + +#[derive(Default)] +pub struct MyGreeter {} + +#[tonic::async_trait] +impl Greeter for MyGreeter { + async fn say_hello( + &self, + request: Request, + ) -> Result, Status> { + println!("Got a request from {:?}", request.remote_addr()); + + // Extract request data + let name = request.into_inner().name; + + // Create empty ErrorDetails struct + let mut err_details = ErrorDetails::new(); + + // Add error details conditionally + if name.is_empty() { + err_details.add_bad_request_violation("name", "name cannot be empty"); + } else if name.len() > 20 { + err_details.add_bad_request_violation("name", "name is too long"); + } + + if err_details.has_bad_request_violations() { + // Add aditional error details if necessary + err_details + .add_help_link("description of link", "https://resource.example.local") + .set_localized_message("en-US", "message for the user"); + + // Generate error status + let status = Status::with_error_details( + Code::InvalidArgument, + "request contains invalid arguments", + err_details, + ); + + return Err(status); + } + + let reply = hello_world::HelloReply { + message: format!("Hello {}!", name), + }; + Ok(Response::new(reply)) + } +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let addr = "[::1]:50051".parse().unwrap(); + let greeter = MyGreeter::default(); + + println!("GreeterServer listening on {}", addr); + + Server::builder() + .add_service(GreeterServer::new(greeter)) + .serve(addr) + .await?; + + Ok(()) +} diff --git a/examples/src/richer-error/server_vec.rs b/examples/src/richer-error/server_vec.rs new file mode 100644 index 000000000..8ecf2e45c --- /dev/null +++ b/examples/src/richer-error/server_vec.rs @@ -0,0 +1,71 @@ +use tonic::{transport::Server, Code, Request, Response, Status}; +use tonic_types::{BadRequest, Help, LocalizedMessage, StatusExt}; + +use hello_world::greeter_server::{Greeter, GreeterServer}; +use hello_world::{HelloReply, HelloRequest}; + +pub mod hello_world { + tonic::include_proto!("helloworld"); +} + +#[derive(Default)] +pub struct MyGreeter {} + +#[tonic::async_trait] +impl Greeter for MyGreeter { + async fn say_hello( + &self, + request: Request, + ) -> Result, Status> { + println!("Got a request from {:?}", request.remote_addr()); + + // Extract request data + let name = request.into_inner().name; + + // Create empty BadRequest struct + let mut bad_request = BadRequest::new(vec![]); + + // Add violations conditionally + if name.is_empty() { + bad_request.add_violation("name", "name cannot be empty"); + } else if name.len() > 20 { + bad_request.add_violation("name", "name is too long"); + } + + if !bad_request.is_empty() { + // Add aditional error details if necessary + let help = Help::with_link("description of link", "https://resource.example.local"); + + let localized_message = LocalizedMessage::new("en-US", "message for the user"); + + // Generate error status + let status = Status::with_error_details_vec( + Code::InvalidArgument, + "request contains invalid arguments", + vec![bad_request.into(), help.into(), localized_message.into()], + ); + + return Err(status); + } + + let reply = hello_world::HelloReply { + message: format!("Hello {}!", name), + }; + Ok(Response::new(reply)) + } +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let addr = "[::1]:50051".parse().unwrap(); + let greeter = MyGreeter::default(); + + println!("GreeterServer listening on {}", addr); + + Server::builder() + .add_service(GreeterServer::new(greeter)) + .serve(addr) + .await?; + + Ok(()) +} diff --git a/tonic-types/src/richer_error/error_details/mod.rs b/tonic-types/src/richer_error/error_details/mod.rs index 1f8f2c398..70074aa9c 100644 --- a/tonic-types/src/richer_error/error_details/mod.rs +++ b/tonic-types/src/richer_error/error_details/mod.rs @@ -382,53 +382,53 @@ impl ErrorDetails { } /// Get [`RetryInfo`] details, if any. - pub fn retry_info(&self) -> Option { - self.retry_info.clone() + pub fn retry_info(&self) -> Option<&RetryInfo> { + self.retry_info.as_ref() } /// Get [`DebugInfo`] details, if any. - pub fn debug_info(&self) -> Option { - self.debug_info.clone() + pub fn debug_info(&self) -> Option<&DebugInfo> { + self.debug_info.as_ref() } /// Get [`QuotaFailure`] details, if any. - pub fn quota_failure(&self) -> Option { - self.quota_failure.clone() + pub fn quota_failure(&self) -> Option<&QuotaFailure> { + self.quota_failure.as_ref() } /// Get [`ErrorInfo`] details, if any. - pub fn error_info(&self) -> Option { - self.error_info.clone() + pub fn error_info(&self) -> Option<&ErrorInfo> { + self.error_info.as_ref() } /// Get [`PreconditionFailure`] details, if any. - pub fn precondition_failure(&self) -> Option { - self.precondition_failure.clone() + pub fn precondition_failure(&self) -> Option<&PreconditionFailure> { + self.precondition_failure.as_ref() } /// Get [`BadRequest`] details, if any. - pub fn bad_request(&self) -> Option { - self.bad_request.clone() + pub fn bad_request(&self) -> Option<&BadRequest> { + self.bad_request.as_ref() } /// Get [`RequestInfo`] details, if any. - pub fn request_info(&self) -> Option { - self.request_info.clone() + pub fn request_info(&self) -> Option<&RequestInfo> { + self.request_info.as_ref() } /// Get [`ResourceInfo`] details, if any. - pub fn resource_info(&self) -> Option { - self.resource_info.clone() + pub fn resource_info(&self) -> Option<&ResourceInfo> { + self.resource_info.as_ref() } /// Get [`Help`] details, if any. - pub fn help(&self) -> Option { - self.help.clone() + pub fn help(&self) -> Option<&Help> { + self.help.as_ref() } /// Get [`LocalizedMessage`] details, if any. - pub fn localized_message(&self) -> Option { - self.localized_message.clone() + pub fn localized_message(&self) -> Option<&LocalizedMessage> { + self.localized_message.as_ref() } /// Set [`RetryInfo`] details. Can be chained with other `.set_` and