Error handling with thiserror and anyhow

This commit is contained in:
Alphonse Paix
2025-08-26 12:47:22 +02:00
parent 4ce25a8136
commit 9193f2020d
5 changed files with 133 additions and 49 deletions

View File

@@ -3,7 +3,13 @@ use crate::{
email_client::EmailClient,
startup::AppState,
};
use axum::{Form, extract::State, http::StatusCode, response::IntoResponse};
use anyhow::Context;
use axum::{
Form, Json,
extract::State,
http::StatusCode,
response::{IntoResponse, Response},
};
use chrono::Utc;
use rand::{Rng, distr::Alphanumeric};
use serde::Deserialize;
@@ -18,6 +24,69 @@ fn generate_subscription_token() -> String {
.collect()
}
fn error_chain_fmt(e: &impl std::error::Error, f: &mut std::fmt::Formatter) -> std::fmt::Result {
writeln!(f, "{}", e)?;
let mut current = e.source();
while let Some(cause) = current {
write!(f, "Caused by:\n\t{}", cause)?;
current = cause.source();
if current.is_some() {
writeln!(f)?;
}
}
Ok(())
}
// #[derive(thiserror::Error)]
// pub enum SubscribeError {
// #[error("Failed to store the confirmation token for a new subscriber.")]
// StoreToken(#[from] StoreTokenError),
// #[error("A database error occured.")]
// Database(#[from] sqlx::Error),
// #[error("Failed to send a confirmation email.")]
// SendEmail(#[from] reqwest::Error),
// #[error("{0}")]
// Validation(String),
// }
#[derive(thiserror::Error)]
pub enum SubscribeError {
#[error(transparent)]
UnexpectedError(#[from] anyhow::Error),
#[error("{0}")]
Validation(String),
}
impl std::fmt::Debug for SubscribeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
error_chain_fmt(self, f)
}
}
impl IntoResponse for SubscribeError {
fn into_response(self) -> Response {
#[derive(serde::Serialize)]
struct ErrorResponse<'a> {
message: &'a str,
}
tracing::error!("{:?}", self);
// let status = match self {
// SubscribeError::StoreToken(_)
// | SubscribeError::Database(_)
// | SubscribeError::SendEmail(_) => StatusCode::INTERNAL_SERVER_ERROR,
// SubscribeError::Validation(_) => StatusCode::BAD_REQUEST,
// };
let status = match self {
SubscribeError::UnexpectedError(_) => StatusCode::INTERNAL_SERVER_ERROR,
SubscribeError::Validation(_) => StatusCode::BAD_REQUEST,
};
let message = "An internal server error occured.";
(status, Json(ErrorResponse { message })).into_response()
}
}
#[tracing::instrument(
name = "Adding a new subscriber",
skip(connection_pool, email_client, base_url, form),
@@ -33,39 +102,32 @@ pub async fn subscribe(
base_url,
}): State<AppState>,
Form(form): Form<FormData>,
) -> impl IntoResponse {
let Ok(mut transaction) = connection_pool.begin().await else {
return StatusCode::INTERNAL_SERVER_ERROR;
};
let Ok(new_subscriber) = form.try_into() else {
return StatusCode::BAD_REQUEST;
};
let Ok(subscriber_id) = insert_subscriber(&mut transaction, &new_subscriber).await else {
return StatusCode::INTERNAL_SERVER_ERROR;
};
let subscription_token = generate_subscription_token();
if store_token(&mut transaction, &subscription_token, &subscriber_id)
) -> Result<Response, SubscribeError> {
let mut transaction = connection_pool
.begin()
.await
.is_err()
{
return StatusCode::INTERNAL_SERVER_ERROR;
}
if let Err(e) = send_confirmation_email(
.context("Failed to acquire a Postgres connection from the pool.")?;
let new_subscriber = form.try_into().map_err(SubscribeError::Validation)?;
let subscriber_id = insert_subscriber(&mut transaction, &new_subscriber)
.await
.context("Failed to insert new subscriber in the database.")?;
let subscription_token = generate_subscription_token();
store_token(&mut transaction, &subscription_token, &subscriber_id)
.await
.context("Failed to store the confirmation token for a new subscriber.")?;
send_confirmation_email(
&email_client,
&new_subscriber,
&base_url,
&subscription_token,
)
.await
{
tracing::error!("Could not send confirmation email: {:?}", e);
return StatusCode::INTERNAL_SERVER_ERROR;
}
if let Err(e) = transaction.commit().await {
tracing::error!("Failed to commit transaction: {:?}", e);
return StatusCode::INTERNAL_SERVER_ERROR;
}
StatusCode::OK
.context("Failed to send a confirmation email.")?;
transaction
.commit()
.await
.context("Failed to commit the database transaction to store a new subscriber.")?;
Ok(StatusCode::OK.into_response())
}
#[tracing::instrument(
@@ -87,10 +149,7 @@ pub async fn insert_subscriber(
new_subscriber.name.as_ref(),
Utc::now()
);
transaction.execute(query).await.map_err(|e| {
tracing::error!("Failed to execute query: {:?}", e);
e
})?;
transaction.execute(query).await?;
Ok(subscriber_id)
}
@@ -111,10 +170,7 @@ async fn store_token(
subscription_token,
subscriber_id,
);
transaction.execute(query).await.map_err(|e| {
tracing::error!("Failed to execute query: {:?}", e);
e
})?;
transaction.execute(query).await?;
Ok(())
}

View File

@@ -62,21 +62,23 @@ pub fn app(connection_pool: PgPool, email_client: EmailClient, base_url: String)
.route("/subscriptions", post(subscribe))
.route("/subscriptions/confirm", get(confirm))
.layer(
TraceLayer::new_for_http().make_span_with(|request: &Request<_>| {
let matched_path = request
.extensions()
.get::<MatchedPath>()
.map(MatchedPath::as_str);
let request_id = Uuid::new_v4().to_string();
TraceLayer::new_for_http()
.make_span_with(|request: &Request<_>| {
let matched_path = request
.extensions()
.get::<MatchedPath>()
.map(MatchedPath::as_str);
let request_id = Uuid::new_v4().to_string();
tracing::info_span!(
"http_request",
method = ?request.method(),
matched_path,
request_id,
some_other_field = tracing::field::Empty,
)
}),
tracing::info_span!(
"http_request",
method = ?request.method(),
matched_path,
request_id,
some_other_field = tracing::field::Empty,
)
})
.on_failure(()),
)
.with_state(app_state)
}