I’m fiddling about with the Rama framework. There’s a hello-world style example in the GitHub repo that I took as a base, adapting it to play around and learn.
Here’s what I came up with:
// src/main.rs
// dependencies
use anyhow::{Context, Result};
use rama::{http::StatusCode, http::server::HttpServer, rt::Executor, service::service_fn};
use std::convert::Infallible;
// handler which returns a 200 OK status code and empty body
async fn health_check() -> Result<StatusCode, Infallible> {
Ok(StatusCode::OK)
}
#[tokio::main]
async fn main() -> Result<()> {
let exec = Executor::default();
HttpServer::auto(exec)
.listen("127.0.0.1:8080", service_fn(health_check))
.await
.map_err(|e| anyhow::anyhow!("{}", e))
.context("Failed to start HTTP server on 127.0.0.1:8080")?;
Ok(())
}
I’ll use this example in a variety of ways going forward, because it illustrates several things. For now, let’s look at the error handling.
In the repo example, the authors simply .unwrap() after the .await to avoid showing the specifics of error handling. This is fine, but I always like to push a little to see if I can work the error handling out.
The await call will eventually resolve into a Result<(), Box<dyn Error + Send + Sync + 'static> where the error is a heap-allocated trait object. This approach adds flexibility. It’s basically saying that the error type can be anything that:
- implements the
Errortrait - is safe to send across threads
- is safe to be mutated across threads
- will always have ownership of itself
The end part of the code above leverages .map_err() which takes our trait object and converts it into an anyhow error, to which we add some extra context and explanation.
There is a small runtime penalty for using trait objects, given they use heap memory, so you have to evaluate that in the context of whatever it is your doing. It might be acceptable.
Trait objects are a great way to enable flexibility in your code and are a great addition to the toolbox.
Comments