Using Rust with Axum for error handling
原文:https://blog.logrocket.com/rust-axum-error-handling/
When you’re building a backend web service, things don’t always go right. For example, an incoming request may not have the necessary data or may present its data in the wrong format. Most web services handle this possibility by returning error responses.
Axum is a framework for building server-side web applications using Rust. It also allows you to handle error responses.
This tutorial gives you a little refresher on building a web service with Rust and Axum, along with how to use this framework to handle error responses.
Jump ahead:
- Installing Axum
- Building a simple web server with Rust and Axum
- Understanding how the simple web server works
- What is a handler?
- Handling errors in Rust with Axum
- Other methods of handling errors in Rust with Axum
Installing Axum
To set up an Axum project, the first thing you need to do is create an empty Rust project with this command:
$ cargo new --bin axum-web-server
The command above creates an axum-web-server
folder and initializes the files you need to get started.
The next thing you need to do is install the axum
and tokio
packages. To install them, open the Cargo.toml
file in the axum-web-server
folder and write the following under [dependencies]
:
axum = "0.6.1"
tokio = { version = "1.23.0", features = ["full"] }
tower = { version = "0.4.13", features = ["timeout"] }
Then build the package with the command below:
$ cargo build
As a reminder, when you’re building a project, cargo
downloads the packages you listed under [dependencies]
from its registry into your project.
Building a simple web server with Axum
With your project set up, it’s time to build a simple application. Later, we will use this simple application to demonstrate how errors are handled in Axum.
If you already have your project and just want to see Axum’s error-handling functionality in action, you can skip ahead.
In this section, the application you’ll build is a web service that has three endpoints:
/
— the root; returns the textWelcome
/say-hello
— returns the textHello!
/say-goodbye
— returns the textGoodbye
The code for this server is below. Copy and paste the code into the src/main.rs
file in the axum-web-server
folder:
use axum::{
routing::get,
Router,
};
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/", get(root))
.route("/say-hello", get(say_hello))
.route("/say-goodbye", get(say_goodbye));
axum::Server::bind(&"127.0.0.1:3000".parse().unwrap())
.serve(app.into_make_service())
.await.unwrap();
}
async fn root() -> String {
return "Welcome!".to_string();
}
async fn say_hello() -> String {
return "Hello!".to_string();
}
async fn say_goodbye() -> String {
return "Goodbye!".to_string();
}
To see the code in action, run the following command:
$ cargo run
The command above runs the main.rs
file. It also starts a local web server at http://127.0.0.1:3000
while the program is running.
When you open http://127.0.0.1:3000
in your browser, you’ll get a message reading Welcome
on the browser page.
When you open http://127.0.0.1:3000/say-hello
instead, you’ll get a message reading Hello!
Opening a URL link in a browser is similar to sending a GET request to the URL. The browser displays the appropriate text response for the request in the browser page.
Understanding how the simple web server works
Now you may be wondering how the code works. The first thing the program does is to import the necessary packages:
use axum::{
routing::get,
Router,
};
The packages includes the following:
routing::get
— for registering GET request handlers to a routeRouter
— for creating and handling routes
Underneath the imports, the program defines the main function:
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/", get(root))
.route("/say-hello", get(say_hello))
.route("/say-goodbye", get(say_goodbye));
axum::Server::bind(&"127.0.0.1:3000".parse().unwrap())
.serve(app.into_make_service())
.await.unwrap();
}
By default, Rust doesn’t allow you to declare the main function as asynchronous, so you need to modify the function with the #[tokio::main]
attribute.
In the main
function, the first thing we did was create a router and register handlers for each route we want the web server to have:
let app = Router::new()
.route("/", get(root))
.route("/say-hello/:name", get(say_hello))
.route("/say-goodbye/:name", get(say_goodbye));
The next thing we did was create a server, bind it to http://127.0.0.1:3000
, and register the router we created:
axum::Server::bind(&"127.0.0.1:3000".parse().unwrap())
.serve(app.into_make_service())
.await.unwrap();
Below the main
function, we declared the root
, say_hello
, and say_goodbye
handlers that we registered to the router:
async fn root() -> String {
return "Welcome!".to_string();
}
async fn say_hello() -> String {
return "Hello!".to_string();
}
async fn say_goodbye() -> String {
return "Goodbye!".to_string();
}
Now, we can move on to error handling in Rust with Axum.
What is a handler?
A handler is an asynchronous function that handles the logic behind a request.
Not only can the handler access the request data sent to the route, but it also provides the response to the request. Once you register a handler on a route, any request sent to the route will be dealt with by the handler.
The root
, say_hello
, and say_goodbye
functions in the earlier sections are simple examples of a handler:
async fn root() -> String {
return "Welcome!".to_string();
}
async fn say_hello() -> String {
return "Hello!".to_string();
}
async fn say_goodbye() -> String {
return "Goodbye!".to_string();
}
At the end of a handler’s operation on a request, what the handler returns will be the server’s response.
For example, when you send a request to http://127.0.0.1:3000
, the program calls root
. Since root
returns Welcome!
, the root handler returns Welcome!
as its response.
Handling errors in Rust with Axum
Besides returning regular responses, you may need to return errors in a response. Errors are useful for letting the client know what went wrong with its request.
In the example below, we modify the root handler to include error handling capabilities:
async fn root() -> Result<String, StatusCode> {
let a_condition = false;
if a_condition {
return Ok("Welcome!".to_string());
} else {
return Err(StatusCode::INTERNAL_SERVER_ERROR);
}
}
As you can see, the handler returns an internal server error response because a_condition
is false.
With root
now modified, when you open http://127.0.0.1:3000/
in a browser, the web page will look like the image below:
If we set a_condition
to true, the handler returns Welcome!
again, as it did previously.
Other methods of handling errors in Rust with Axum
Axum provides other ways to handle error responses besides Result::Err
enums. Two of the major ways are fallible handlers and pattern matching.
Error handling with fallible handlers
You may not always know every error condition in a handler, and a handler can run into an error at runtime. However, Axum requires all handlers to be infallible. As a result, when the handler runs into an error, the incoming request won’t get a response.
A fallible handler returns an error response if the handler runs into an error. The following is an example of a fallible handler:
use axum::{
Router,
http::{Response, StatusCode},
error_handling::HandleError,
};
#[tokio::main]
async fn main() {
// this service might fail with `reqwest::Error`
let fallible_service = tower::service_fn(|_req| async {
let body = can_fail().await?;
Ok::<_, reqwest::Error>(Response::new(body))
});
// Since fallible_service can fail with 'reqwest::Error',
// you can't directly route it to "/".
// Use route_service to convert any errors
// encountered into a response
let app = Router::new()
.route_service("/", HandleError::new(fallible_service, handle_error) );
axum::Server::bind(&"127.0.0.1:3000".parse().unwrap())
.serve(app.into_make_service())
.await
.unwrap();
}
async fn can_fail() -> Result<String, reqwest::Error> {
// send a request to a site that doesn't exist
// so we can see the handler fail
let body = reqwest::get("https://www.abcdth.org")
.await?
.text()
.await?;
Ok(body)
}
async fn handle_error(err: reqwest::Error) -> (StatusCode, String) {
return (
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {}", err),
);
}
As you can see, the fallible_service
will fail and return an internal service error. You should then see a message that reads Something went wrong:
followed by an error message that explains what went wrong:
Error handling with pattern matching
When an Axum server fails, it returns a no-content response. A server failure usually originates from statements that have the potential to fail.
To prevent the server from returning a no-content response, we need to handle the errors directly. A direct method of handling errors is through pattern matching.
To handle an error using pattern matching, simply wrap the statement where a potential error can occur with a match
block.
In the example below, the reqwest
statement is wrapped because it can fail:
use axum::{
Router,
routing::get,
http::StatusCode,
};
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/", get(|| async {
match reqwest::get("https://www.abcdth.org").await {
Ok(res) => (
StatusCode::OK,
res.text().await.unwrap(),
),
Err(err) => (
StatusCode::INTERNAL_SERVER_ERROR,
format!("Server failed with {}", err),
)
}
}));
axum::Server::bind(&"127.0.0.1:3000".parse().unwrap())
.serve(app.into_make_service())
.await
.unwrap();
}
As you can see, when the handler fails, it returns an internal service error, then displays a message that explains why it failed:
Conclusion
In this article, we looked at how to build a web service with Rust and Axum, along with how error handling works and the different ways you can handle error responses in Axum.
Axum is a powerful tool for building web applications and services. Learn more about the framework with the Rust docs on Axum, this article on how to deploy a Rust web server to Heroku, or the Rust docs on Axum status codes.
转载请注明原文链接:https://www.cnblogs.com/itfanr/p/17135422.html
公众号:小弧光黑板报