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"
|
path = "src/json-codec/server.rs"
|
||||||
required-features = ["json-codec"]
|
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]
|
[features]
|
||||||
gcp = ["dep:prost-types", "tonic/tls"]
|
gcp = ["dep:prost-types", "tonic/tls"]
|
||||||
routeguide = ["dep:async-stream", "dep:futures", "tokio-stream", "dep:rand", "dep:serde", "dep:serde_json"]
|
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"]
|
dynamic-load-balance = ["dep:tower"]
|
||||||
timeout = ["tokio/time", "dep:tower"]
|
timeout = ["tokio/time", "dep:tower"]
|
||||||
tls-client-auth = ["tonic/tls"]
|
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"]
|
default = ["full"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
@@ -267,6 +288,7 @@ tonic = { path = "../tonic" }
|
|||||||
tonic-web = { path = "../tonic-web", optional = true }
|
tonic-web = { path = "../tonic-web", optional = true }
|
||||||
tonic-health = { path = "../tonic-health", optional = true }
|
tonic-health = { path = "../tonic-health", optional = true }
|
||||||
tonic-reflection = { path = "../tonic-reflection", optional = true }
|
tonic-reflection = { path = "../tonic-reflection", optional = true }
|
||||||
|
tonic-types = { path = "../tonic-types", optional = true }
|
||||||
async-stream = { version = "0.3", optional = true }
|
async-stream = { version = "0.3", optional = true }
|
||||||
futures = { version = "0.3", default-features = false, optional = true }
|
futures = { version = "0.3", default-features = false, optional = true }
|
||||||
tokio-stream = { version = "0.1", 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)
|
* [systemfd](https://crates.io/crates/systemfd)
|
||||||
* [cargo-watch](https://crates.io/crates/cargo-watch)
|
* [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.
|
/// Get [`RetryInfo`] details, if any.
|
||||||
pub fn retry_info(&self) -> Option<RetryInfo> {
|
pub fn retry_info(&self) -> Option<&RetryInfo> {
|
||||||
self.retry_info.clone()
|
self.retry_info.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get [`DebugInfo`] details, if any.
|
/// Get [`DebugInfo`] details, if any.
|
||||||
pub fn debug_info(&self) -> Option<DebugInfo> {
|
pub fn debug_info(&self) -> Option<&DebugInfo> {
|
||||||
self.debug_info.clone()
|
self.debug_info.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get [`QuotaFailure`] details, if any.
|
/// Get [`QuotaFailure`] details, if any.
|
||||||
pub fn quota_failure(&self) -> Option<QuotaFailure> {
|
pub fn quota_failure(&self) -> Option<&QuotaFailure> {
|
||||||
self.quota_failure.clone()
|
self.quota_failure.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get [`ErrorInfo`] details, if any.
|
/// Get [`ErrorInfo`] details, if any.
|
||||||
pub fn error_info(&self) -> Option<ErrorInfo> {
|
pub fn error_info(&self) -> Option<&ErrorInfo> {
|
||||||
self.error_info.clone()
|
self.error_info.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get [`PreconditionFailure`] details, if any.
|
/// Get [`PreconditionFailure`] details, if any.
|
||||||
pub fn precondition_failure(&self) -> Option<PreconditionFailure> {
|
pub fn precondition_failure(&self) -> Option<&PreconditionFailure> {
|
||||||
self.precondition_failure.clone()
|
self.precondition_failure.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get [`BadRequest`] details, if any.
|
/// Get [`BadRequest`] details, if any.
|
||||||
pub fn bad_request(&self) -> Option<BadRequest> {
|
pub fn bad_request(&self) -> Option<&BadRequest> {
|
||||||
self.bad_request.clone()
|
self.bad_request.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get [`RequestInfo`] details, if any.
|
/// Get [`RequestInfo`] details, if any.
|
||||||
pub fn request_info(&self) -> Option<RequestInfo> {
|
pub fn request_info(&self) -> Option<&RequestInfo> {
|
||||||
self.request_info.clone()
|
self.request_info.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get [`ResourceInfo`] details, if any.
|
/// Get [`ResourceInfo`] details, if any.
|
||||||
pub fn resource_info(&self) -> Option<ResourceInfo> {
|
pub fn resource_info(&self) -> Option<&ResourceInfo> {
|
||||||
self.resource_info.clone()
|
self.resource_info.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get [`Help`] details, if any.
|
/// Get [`Help`] details, if any.
|
||||||
pub fn help(&self) -> Option<Help> {
|
pub fn help(&self) -> Option<&Help> {
|
||||||
self.help.clone()
|
self.help.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get [`LocalizedMessage`] details, if any.
|
/// Get [`LocalizedMessage`] details, if any.
|
||||||
pub fn localized_message(&self) -> Option<LocalizedMessage> {
|
pub fn localized_message(&self) -> Option<&LocalizedMessage> {
|
||||||
self.localized_message.clone()
|
self.localized_message.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set [`RetryInfo`] details. Can be chained with other `.set_` and
|
/// Set [`RetryInfo`] details. Can be chained with other `.set_` and
|
||||||
|
|||||||
Reference in New Issue
Block a user