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:
+23
-1
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
```
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user