Skip to content

Commit

Permalink
feat(types): Add gRPC Richer Error Model support (Examples) (#1300)
Browse files Browse the repository at this point in the history
* types: remove unnecessary cloning from `ErrorDetails` getters

Breaking change.

* examples: add `richer-error` examples

Following implementation at flemosr/tonic-richer-error.
  • Loading branch information
flemosr authored Mar 14, 2023
1 parent c8027a1 commit d471212
Show file tree
Hide file tree
Showing 7 changed files with 325 additions and 21 deletions.
24 changes: 23 additions & 1 deletion examples/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand All @@ -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]
Expand All @@ -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 }
Expand Down
30 changes: 30 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 52 additions & 0 deletions examples/src/richer-error/client.rs
Original file line number Diff line number Diff line change
@@ -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 changes: 58 additions & 0 deletions examples/src/richer-error/client_vec.rs
Original file line number Diff line number Diff line change
@@ -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 changes: 71 additions & 0 deletions examples/src/richer-error/server.rs
Original file line number Diff line number Diff line change
@@ -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 changes: 71 additions & 0 deletions examples/src/richer-error/server_vec.rs
Original file line number Diff line number Diff line change
@@ -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(())
}
Loading

0 comments on commit d471212

Please sign in to comment.