feat(types): Add gRPC Richer Error Model support (Examples) (#1300)

* types: remove unnecessary cloning from `ErrorDetails` getters

Breaking change.

* examples: add `richer-error` examples

Following implementation at flemosr/tonic-richer-error.
This commit is contained in:
Rafael Lemos
2023-03-14 12:26:35 -03:00
committed by GitHub
parent c8027a1385
commit d471212ee8
7 changed files with 325 additions and 21 deletions
+23 -1
View File
@@ -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 }
+30
View File
@@ -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
```
+52
View File
@@ -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<dyn std::error::Error>> {
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(())
}
+58
View File
@@ -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<dyn std::error::Error>> {
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(())
}
+71
View File
@@ -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<HelloRequest>,
) -> Result<Response<HelloReply>, 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<dyn std::error::Error>> {
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(())
}
+71
View File
@@ -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<HelloRequest>,
) -> Result<Response<HelloReply>, 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<dyn std::error::Error>> {
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(())
}
@@ -382,53 +382,53 @@ impl ErrorDetails {
}
/// Get [`RetryInfo`] details, if any.
pub fn retry_info(&self) -> Option<RetryInfo> {
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<DebugInfo> {
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<QuotaFailure> {
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<ErrorInfo> {
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<PreconditionFailure> {
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<BadRequest> {
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<RequestInfo> {
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<ResourceInfo> {
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<Help> {
self.help.clone()
pub fn help(&self) -> Option<&Help> {
self.help.as_ref()
}
/// Get [`LocalizedMessage`] details, if any.
pub fn localized_message(&self) -> Option<LocalizedMessage> {
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