mirror of
https://github.com/AThilenius/axum-connect.git
synced 2024-11-24 06:19:46 +00:00
Update example to include error type and more detail
This commit is contained in:
parent
cd33439672
commit
eb39acaf30
5 changed files with 182 additions and 29 deletions
79
README.md
79
README.md
|
@ -112,9 +112,20 @@ With the boring stuff out of the way, let's implement our service using Axum!
|
||||||
use async_stream::stream;
|
use async_stream::stream;
|
||||||
use axum::{extract::Host, Router};
|
use axum::{extract::Host, Router};
|
||||||
use axum_connect::{futures::Stream, prelude::*};
|
use axum_connect::{futures::Stream, prelude::*};
|
||||||
|
use error::Error;
|
||||||
use proto::hello::*;
|
use proto::hello::*;
|
||||||
|
use tower_http::cors::CorsLayer;
|
||||||
|
|
||||||
|
// Take a peak at error.rs to see how errors work in axum-connect.
|
||||||
|
mod error;
|
||||||
|
|
||||||
mod proto {
|
mod proto {
|
||||||
|
// Include the generated code in a `proto` module.
|
||||||
|
//
|
||||||
|
// Note: I'm not super happy with this pattern. I hope to add support to `protoc-gen-prost` in
|
||||||
|
// the near-ish future instead see:
|
||||||
|
// https://github.com/neoeinstein/protoc-gen-prost/issues/82#issuecomment-1877107220 That will
|
||||||
|
// better align with Buf.build's philosophy. This is how it works for now though.
|
||||||
pub mod hello {
|
pub mod hello {
|
||||||
include!(concat!(env!("OUT_DIR"), "/hello.rs"));
|
include!(concat!(env!("OUT_DIR"), "/hello.rs"));
|
||||||
}
|
}
|
||||||
|
@ -126,41 +137,75 @@ async fn main() {
|
||||||
// It expect a service method handler, wrapped in it's respective type. The handler (below) is
|
// It expect a service method handler, wrapped in it's respective type. The handler (below) is
|
||||||
// just a normal Rust function. Just like Axum, it also supports extractors!
|
// just a normal Rust function. Just like Axum, it also supports extractors!
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.rpc(HelloWorldService::say_hello(say_hello_success))
|
// A standard unary (POST based) Connect-Web request handler.
|
||||||
.rpc(HelloWorldService::say_hello_stream(say_hello_stream));
|
.rpc(HelloWorldService::say_hello(say_hello_unary))
|
||||||
|
// A GET version of the same thing, which has well-defined semantics for caching.
|
||||||
|
.rpc(HelloWorldService::say_hello_unary_get(say_hello_unary))
|
||||||
|
// A server-streaming request handler. Very useful when you need them!
|
||||||
|
.rpc(HelloWorldService::say_hello_stream(stream_three_reponses));
|
||||||
|
|
||||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:3030")
|
let listener = tokio::net::TcpListener::bind("127.0.0.1:3030")
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
println!("listening on http://{:?}", listener.local_addr());
|
println!("listening on http://{:?}", listener.local_addr().unwrap());
|
||||||
axum::serve(listener, app).await.unwrap();
|
axum::serve(listener, app.layer(CorsLayer::very_permissive()))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn say_hello_success(
|
/// The bread-and-butter of Connect-Web, a Unary request handler.
|
||||||
Host(host): Host,
|
///
|
||||||
request: HelloRequest
|
/// Just to demo error handling, I've chose to return a `Result` here. If your method is
|
||||||
) -> HelloResponse {
|
/// infallible, you could just as easily return a `HellResponse` directly. The error type I'm using
|
||||||
HelloResponse {
|
/// is defined in `error.rs` and is worth taking a quick peak at.
|
||||||
|
///
|
||||||
|
/// Like Axum, both the request AND response types just need to implement RpcFromRequestParts` and
|
||||||
|
/// `RpcIntoResponse` respectively. This allows for a ton of flexibility in what your handlers
|
||||||
|
/// actually accept/return. This is a concept very core to Axum, so I won't go too deep into the
|
||||||
|
/// ideology here.
|
||||||
|
async fn say_hello_unary(Host(host): Host, request: HelloRequest) -> Result<HelloResponse, Error> {
|
||||||
|
Ok(HelloResponse {
|
||||||
message: format!(
|
message: format!(
|
||||||
"Hello {}! You're addressing the hostname: {}.",
|
"Hello {}! You're addressing the hostname: {}.",
|
||||||
request.name, host
|
request.name.unwrap_or_else(|| "unnamed".to_string()),
|
||||||
|
host
|
||||||
),
|
),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This is a server-streaming request handler. Much more rare to see one in the wild, but they
|
||||||
|
/// sure are useful when you need them! axum-connect has only partial support for everything
|
||||||
|
/// connect-web defines in server-streaming requests. For example, it doesn't define a way to
|
||||||
|
/// return trailers. I've never once actually needed them, so it feels weird to muddy the API just
|
||||||
|
/// to support such a niche use. Trailers are IMO the worst single decision gRPC made, locking them
|
||||||
|
/// into HTTP/2 forever. I'm not a fan -.-
|
||||||
|
///
|
||||||
|
/// You can however return a stream of anything that converts `RpcIntoResponse`, just like the
|
||||||
|
/// unary handlers. Again, very flexible. In this case I'm using the amazing `async-stream` crate
|
||||||
|
/// to make the code nice and readable.
|
||||||
|
async fn stream_three_reponses(
|
||||||
|
Host(host): Host,
|
||||||
|
request: HelloRequest,
|
||||||
|
) -> impl Stream<Item = HelloResponse> {
|
||||||
|
stream! {
|
||||||
|
yield HelloResponse { message: "Hello".to_string() };
|
||||||
|
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||||
|
yield HelloResponse { message: request.name().to_string() };
|
||||||
|
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||||
|
yield HelloResponse { message: format!("You're addressing the hostname: {}", host) };
|
||||||
|
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## SEND IT 🚀
|
## SEND IT 🚀
|
||||||
|
|
||||||
To test it out, try hitting the endpoint manually.
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
curl --location 'http://localhost:3030/hello.HelloWorldService/SayHello' \
|
cargo run -p axum-connect-example
|
||||||
--header 'Content-Type: application/json' \
|
|
||||||
--data '{ "name": "Alec" }'
|
|
||||||
```
|
```
|
||||||
|
|
||||||
From here you can stand up a `connect-web` TypeScript/Go project to call your
|
It's Connect RPCs, so you can use the Buf Studio to test things out!
|
||||||
API with end-to-end typed RPCs.
|
https://buf.build/studio/athilenius/axum-connect/main/hello.HelloWorldService/SayHello?target=http%3A%2F%2Flocalhost%3A3030
|
||||||
|
|
||||||
# Request/Response Parts 🙍♂️
|
# Request/Response Parts 🙍♂️
|
||||||
|
|
||||||
|
|
|
@ -4,11 +4,14 @@ version = "0.2.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
anyhow = "1.0.80"
|
||||||
async-stream = "0.3.5"
|
async-stream = "0.3.5"
|
||||||
axum = "0.7.2"
|
axum = "0.7.2"
|
||||||
axum-connect = { path = "../axum-connect" }
|
axum-connect = { path = "../axum-connect" }
|
||||||
prost = "0.12.1"
|
prost = "0.12.1"
|
||||||
|
thiserror = "1.0.57"
|
||||||
tokio = { version = "1.0", features = ["full"] }
|
tokio = { version = "1.0", features = ["full"] }
|
||||||
|
tower-http = { version = "0.5.1", features = ["cors"] }
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
axum-connect-build = { path = "../axum-connect-build" }
|
axum-connect-build = { path = "../axum-connect-build" }
|
||||||
|
|
8
axum-connect-examples/proto/buf.yaml
Normal file
8
axum-connect-examples/proto/buf.yaml
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
version: v1
|
||||||
|
name: buf.build/athilenius/axum-connect
|
||||||
|
breaking:
|
||||||
|
use:
|
||||||
|
- FILE
|
||||||
|
lint:
|
||||||
|
use:
|
||||||
|
- DEFAULT
|
62
axum-connect-examples/src/error.rs
Normal file
62
axum-connect-examples/src/error.rs
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
use axum::{
|
||||||
|
http::StatusCode,
|
||||||
|
response::{IntoResponse, Response},
|
||||||
|
};
|
||||||
|
use axum_connect::error::{RpcError, RpcErrorCode, RpcIntoError};
|
||||||
|
|
||||||
|
// This is an example Error type, to demo impls needed for `axum-connect`. It uses `thiserror` to
|
||||||
|
// wrap various error types as syntactic sugar, but you could just as easily write this out by hand.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
/// Returns `403 Forbidden`
|
||||||
|
#[error("user may not perform that action")]
|
||||||
|
Forbidden,
|
||||||
|
|
||||||
|
/// Returns `404 Not Found`
|
||||||
|
#[error("request path not found")]
|
||||||
|
NotFound,
|
||||||
|
|
||||||
|
/// Returns `500 Internal Server Error`
|
||||||
|
#[error("an internal server error occurred")]
|
||||||
|
Anyhow(#[from] anyhow::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Allows the error type to be returned from RPC handlers.
|
||||||
|
///
|
||||||
|
/// This trait is distinct from `IntoResponse` because RPCs cannot return arbitrary HTML responses.
|
||||||
|
/// Error codes are well-defined in connect-web (which mirrors gRPC), streaming errors don't effect
|
||||||
|
/// HTTP status codes, and so on.
|
||||||
|
impl RpcIntoError for Error {
|
||||||
|
fn rpc_into_error(self) -> axum_connect::prelude::RpcError {
|
||||||
|
println!("{:#?}", self);
|
||||||
|
|
||||||
|
// Each response is a tuple of well-defined (per the Connect-Web) codes, along with a
|
||||||
|
// message.
|
||||||
|
match self {
|
||||||
|
Self::Forbidden => {
|
||||||
|
RpcError::new(RpcErrorCode::PermissionDenied, "Forbidden".to_string())
|
||||||
|
}
|
||||||
|
Self::NotFound => RpcError::new(RpcErrorCode::NotFound, "Not Found".to_string()),
|
||||||
|
Self::Anyhow(_) => {
|
||||||
|
RpcError::new(RpcErrorCode::Internal, "Internal Server Error".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is a normal `IntoResponse` impl, which is used by Axum to convert errors into HTTP
|
||||||
|
// responses.
|
||||||
|
impl IntoResponse for Error {
|
||||||
|
fn into_response(self) -> Response {
|
||||||
|
println!("{:#?}", self);
|
||||||
|
match self {
|
||||||
|
Self::Forbidden => (StatusCode::FORBIDDEN, "Forbidden").into_response(),
|
||||||
|
Self::NotFound => (StatusCode::NOT_FOUND, "Not Found").into_response(),
|
||||||
|
Self::Anyhow(_) => {
|
||||||
|
(StatusCode::INTERNAL_SERVER_ERROR, "Internal Server Error").into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,16 +1,26 @@
|
||||||
//!
|
//!
|
||||||
//! $ cargo run -p axum-connect-example
|
//! $ cargo run -p axum-connect-example
|
||||||
//!
|
//! Head over to Buf Studio to test out RPCs, with auto-completion!
|
||||||
//! $ curl 'http://127.0.0.1:3030/hello.HelloWorldService/SayHello?encoding=json&message=%7B%7D' -v
|
//! https://buf.build/studio/athilenius/axum-connect/main/hello.HelloWorldService/SayHello?target=http%3A%2F%2Flocalhost%3A3030
|
||||||
//! $ curl 'http://127.0.0.1:3030/hello.HelloWorldService/SayHello?encoding=json&message=%7B%22name%22%3A%22foo%22%7D' -v
|
|
||||||
//!
|
//!
|
||||||
|
|
||||||
use async_stream::stream;
|
use async_stream::stream;
|
||||||
use axum::{extract::Host, Router};
|
use axum::{extract::Host, Router};
|
||||||
use axum_connect::{futures::Stream, prelude::*};
|
use axum_connect::{futures::Stream, prelude::*};
|
||||||
|
use error::Error;
|
||||||
use proto::hello::*;
|
use proto::hello::*;
|
||||||
|
use tower_http::cors::CorsLayer;
|
||||||
|
|
||||||
|
// Take a peak at error.rs to see how errors work in axum-connect.
|
||||||
|
mod error;
|
||||||
|
|
||||||
mod proto {
|
mod proto {
|
||||||
|
// Include the generated code in a `proto` module.
|
||||||
|
//
|
||||||
|
// Note: I'm not super happy with this pattern. I hope to add support to `protoc-gen-prost` in
|
||||||
|
// the near-ish future instead see:
|
||||||
|
// https://github.com/neoeinstein/protoc-gen-prost/issues/82#issuecomment-1877107220 That will
|
||||||
|
// better align with Buf.build's philosophy. This is how it works for now though.
|
||||||
pub mod hello {
|
pub mod hello {
|
||||||
include!(concat!(env!("OUT_DIR"), "/hello.rs"));
|
include!(concat!(env!("OUT_DIR"), "/hello.rs"));
|
||||||
}
|
}
|
||||||
|
@ -22,28 +32,53 @@ async fn main() {
|
||||||
// It expect a service method handler, wrapped in it's respective type. The handler (below) is
|
// It expect a service method handler, wrapped in it's respective type. The handler (below) is
|
||||||
// just a normal Rust function. Just like Axum, it also supports extractors!
|
// just a normal Rust function. Just like Axum, it also supports extractors!
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.rpc(HelloWorldService::say_hello(say_hello_success))
|
// A standard unary (POST based) Connect-Web request handler.
|
||||||
.rpc(HelloWorldService::say_hello_unary_get(say_hello_success))
|
.rpc(HelloWorldService::say_hello(say_hello_unary))
|
||||||
.rpc(HelloWorldService::say_hello_stream(say_hello_stream));
|
// A GET version of the same thing, which has well-defined semantics for caching.
|
||||||
|
.rpc(HelloWorldService::say_hello_unary_get(say_hello_unary))
|
||||||
|
// A server-streaming request handler. Very useful when you need them!
|
||||||
|
.rpc(HelloWorldService::say_hello_stream(stream_three_reponses));
|
||||||
|
|
||||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:3030")
|
let listener = tokio::net::TcpListener::bind("127.0.0.1:3030")
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
println!("listening on http://{:?}", listener.local_addr());
|
println!("listening on http://{:?}", listener.local_addr().unwrap());
|
||||||
axum::serve(listener, app).await.unwrap();
|
axum::serve(listener, app.layer(CorsLayer::very_permissive()))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn say_hello_success(Host(host): Host, request: HelloRequest) -> HelloResponse {
|
/// The bread-and-butter of Connect-Web, a Unary request handler.
|
||||||
HelloResponse {
|
///
|
||||||
|
/// Just to demo error handling, I've chose to return a `Result` here. If your method is
|
||||||
|
/// infallible, you could just as easily return a `HellResponse` directly. The error type I'm using
|
||||||
|
/// is defined in `error.rs` and is worth taking a quick peak at.
|
||||||
|
///
|
||||||
|
/// Like Axum, both the request AND response types just need to implement RpcFromRequestParts` and
|
||||||
|
/// `RpcIntoResponse` respectively. This allows for a ton of flexibility in what your handlers
|
||||||
|
/// actually accept/return. This is a concept very core to Axum, so I won't go too deep into the
|
||||||
|
/// ideology here.
|
||||||
|
async fn say_hello_unary(Host(host): Host, request: HelloRequest) -> Result<HelloResponse, Error> {
|
||||||
|
Ok(HelloResponse {
|
||||||
message: format!(
|
message: format!(
|
||||||
"Hello {}! You're addressing the hostname: {}.",
|
"Hello {}! You're addressing the hostname: {}.",
|
||||||
request.name.unwrap_or_else(|| "unnamed".to_string()),
|
request.name.unwrap_or_else(|| "unnamed".to_string()),
|
||||||
host
|
host
|
||||||
),
|
),
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn say_hello_stream(
|
/// This is a server-streaming request handler. Much more rare to see one in the wild, but they
|
||||||
|
/// sure are useful when you need them! axum-connect has only partial support for everything
|
||||||
|
/// connect-web defines in server-streaming requests. For example, it doesn't define a way to
|
||||||
|
/// return trailers. I've never once actually needed them, so it feels weird to muddy the API just
|
||||||
|
/// to support such a niche use. Trailers are IMO the worst single decision gRPC made, locking them
|
||||||
|
/// into HTTP/2 forever. I'm not a fan -.-
|
||||||
|
///
|
||||||
|
/// You can however return a stream of anything that converts `RpcIntoResponse`, just like the
|
||||||
|
/// unary handlers. Again, very flexible. In this case I'm using the amazing `async-stream` crate
|
||||||
|
/// to make the code nice and readable.
|
||||||
|
async fn stream_three_reponses(
|
||||||
Host(host): Host,
|
Host(host): Host,
|
||||||
request: HelloRequest,
|
request: HelloRequest,
|
||||||
) -> impl Stream<Item = HelloResponse> {
|
) -> impl Stream<Item = HelloResponse> {
|
||||||
|
|
Loading…
Reference in a new issue