Error handling with thiserror and anyhow
This commit is contained in:
@@ -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(())
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user