Skip to content

Commit

Permalink
Merge pull request #7 from djdongjin/ch8-error-handle
Browse files Browse the repository at this point in the history
Ch8 error handle
  • Loading branch information
djdongjin committed Apr 3, 2024
2 parents f4d85af + a924871 commit c6280d6
Show file tree
Hide file tree
Showing 5 changed files with 155 additions and 78 deletions.
8 changes: 8 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ name = "zero2prod"

[dependencies]
actix-web = "4.5.1"
anyhow = "1.0.81"
chrono = { version = "0.4.35", default-features = false, features = ["clock"] }
config = "0.14.0"
log = "0.4.21"
Expand All @@ -24,6 +25,7 @@ reqwest = { version = "0.12.1", default-features = false, features = ["json", "r
secrecy = { version = "0.8.0", features = ["serde"] }
serde = { version = "1.0.197", features = ["derive"] }
serde-aux = "4.5.0"
thiserror = "1.0.58"
tokio = { version = "1.36.0", features = ["macros", "rt-multi-thread", "rt"] }
tracing = { version = "0.1.40", features = ["log"] }
tracing-actix-web = "0.7.10"
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,10 @@ To disable a clippy warning: `#[allow(clippy::lint_name)]`
### Ch6

- [ ] `.into()` method
- [ ] `?` operator

### Ch8 - Error handling

- [ ] `std::fmt::Debug` and `std::fmt::Display`.
- [ ] `?` v.s. `unwrap()`.
- [ ] `async`/`await`.
147 changes: 96 additions & 51 deletions src/routes/subscriptions.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
use actix_web::{web, HttpResponse, Responder};
use crate::domain::{NewSubscriber, SubscriberEmail, SubscriberName};
use crate::email_client::EmailClient;
use crate::startup::ApplicationBaseUrl;
use actix_web::http::StatusCode;
use actix_web::{web, HttpResponse, ResponseError};
use anyhow::Context;
use chrono::Utc;
use rand::{distributions::Alphanumeric, thread_rng, Rng};
use rand::distributions::Alphanumeric;
use rand::{thread_rng, Rng};
use sqlx::{Executor, PgPool, Postgres, Transaction};
use std::convert::{TryFrom, TryInto};
use uuid::Uuid;

use crate::{
domain::{NewSubscriber, SubscriberEmail, SubscriberName},
email_client::EmailClient,
startup::ApplicationBaseUrl,
};

#[derive(serde::Deserialize)]
pub struct FormData {
email: String,
Expand All @@ -27,6 +28,29 @@ impl TryFrom<FormData> for NewSubscriber {
}
}

#[derive(thiserror::Error)]
pub enum SubscribeError {
#[error("{0}")]
ValidationError(String),
#[error(transparent)]
UnexpectedError(#[from] anyhow::Error),
}

impl std::fmt::Debug for SubscribeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
error_chain_fmt(self, f)
}
}

impl ResponseError for SubscribeError {
fn status_code(&self) -> StatusCode {
match self {
SubscribeError::ValidationError(_) => StatusCode::BAD_REQUEST,
SubscribeError::UnexpectedError(_) => StatusCode::INTERNAL_SERVER_ERROR,
}
}
}

#[tracing::instrument(
name = "Adding a new subscriber", // span message, fn name by default
skip(form, db_pool, email_client), // skip these two fields in the span
Expand All @@ -40,49 +64,43 @@ pub async fn subscribe(
db_pool: web::Data<PgPool>,
email_client: web::Data<EmailClient>,
base_url: web::Data<ApplicationBaseUrl>,
) -> impl Responder {
) -> Result<HttpResponse, SubscribeError> {
// `web::Form` is a wrapper around `FormData`
// `form.0` gives us access to the underlying `FormData`
let new_subscriber = match form.0.try_into() {
Ok(sub) => sub,
Err(_) => return HttpResponse::BadRequest().finish(),
};

let mut txn = match db_pool.begin().await {
Ok(t) => t,
Err(_) => return HttpResponse::InternalServerError().finish(),
};

let subscriber_id = match insert_subscriber(&new_subscriber, &mut txn).await {
Ok(id) => id,
Err(_) => return HttpResponse::InternalServerError().finish(),
};
let subscription_token = generate_subscription_token();
let new_subscriber = form.0.try_into().map_err(SubscribeError::ValidationError)?;

// the `.context` calls
// 1. converts errors returned by other methods into an `anyhow::Error`;
// 2. adds context to the error message
//
// Also since we defined `UnexpectedError(#[from] anyhow::Error),`, it will
// be converted into an `UnexpectedError` automatically.

if store_token(&mut txn, subscriber_id, &subscription_token)
let mut txn = db_pool
.begin()
.await
.is_err()
{
return HttpResponse::InternalServerError().finish();
}
.context("Failed to acquire a Postgres connection from the pool")?;
let subscriber_id = insert_subscriber(&new_subscriber, &mut txn)
.await
.context("Failed to insert a new subscriber in the database")?;
let subscription_token = generate_subscription_token();
store_token(&mut txn, subscriber_id, &subscription_token)
.await
.context("Failed to store the subscription token in the database")?;
txn.commit()
.await
.context("Failed to commit the transaction")?;

if send_confirmation_email(
send_confirmation_email(
new_subscriber,
&email_client,
&base_url.0,
&subscription_token,
)
.await
.is_err()
{
return HttpResponse::InternalServerError().finish();
}
.context("Failed to send a confirmation email")?;

if txn.commit().await.is_err() {
return HttpResponse::InternalServerError().finish();
}

HttpResponse::Ok().finish()
Ok(HttpResponse::Ok().finish())
}

#[tracing::instrument(
Expand All @@ -102,13 +120,7 @@ async fn insert_subscriber(
form.name.as_ref(),
Utc::now()
);
txn.execute(query).await.map_err(|e| {
tracing::error!("Failed to execute query: {:?}", e);
e
// Using the `?` operator to return early
// if the function failed, returning a sqlx::Error
// We will talk about error handling in depth later!
})?;
txn.execute(query).await?;
Ok(uuid)
}

Expand Down Expand Up @@ -149,24 +161,57 @@ pub async fn store_token(
txn: &mut Transaction<'_, Postgres>,
subscriber_id: Uuid,
subscriber_token: &str,
) -> Result<(), sqlx::Error> {
) -> Result<(), StoreTokenError> {
let query = sqlx::query!(
r#"INSERT INTO subscription_tokens (subscription_token, subscriber_id)
VALUES ($1, $2)"#,
subscriber_token,
subscriber_id
);
txn.execute(query).await.map_err(|e| {
tracing::error!("Failed to execute query: {:?}", e);
e
})?;
txn.execute(query).await.map_err(StoreTokenError)?;
Ok(())
}

pub struct StoreTokenError(sqlx::Error);

impl std::error::Error for StoreTokenError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(&self.0)
}
}

impl std::fmt::Debug for StoreTokenError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
error_chain_fmt(self, f)
}
}

impl std::fmt::Display for StoreTokenError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"A database failure was encountered while trying to store a subscription token."
)
}
}

fn generate_subscription_token() -> String {
let mut rng = thread_rng();
std::iter::repeat_with(|| rng.sample(Alphanumeric))
.map(char::from)
.take(25)
.collect()
}

pub fn error_chain_fmt(
e: &impl std::error::Error,
f: &mut std::fmt::Formatter<'_>,
) -> std::fmt::Result {
writeln!(f, "{}\n", e)?;
let mut current = e.source();
while let Some(cause) = current {
writeln!(f, "Caused by:\n\t{}", cause)?;
current = cause.source();
}
Ok(())
}
69 changes: 42 additions & 27 deletions src/routes/subscriptions_confirm.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
use actix_web::{web, HttpResponse, Responder};
use crate::routes::error_chain_fmt;
use actix_web::http::StatusCode;
use actix_web::{web, HttpResponse, ResponseError};
use anyhow::Context;
use sqlx::PgPool;
use uuid::Uuid;

Expand All @@ -7,24 +10,46 @@ pub struct Parameters {
pub subscription_token: String,
}

#[tracing::instrument(name = "Confirm a pending subscriber", skip(params, db_pool))]
pub async fn confirm(params: web::Query<Parameters>, db_pool: web::Data<PgPool>) -> impl Responder {
let id = match get_subscriber_id_from_token(&db_pool, &params.subscription_token).await {
Ok(id) => id,
Err(_) => return HttpResponse::BadRequest().finish(),
};
#[derive(thiserror::Error)]
pub enum ConfirmationError {
#[error(transparent)]
UnexpectedError(#[from] anyhow::Error),
#[error("There is no subscriber associated with the provided token.")]
UnknownToken,
}

impl std::fmt::Debug for ConfirmationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
error_chain_fmt(self, f)
}
}

match id {
None => HttpResponse::Unauthorized().finish(),
Some(subscriber_id) => {
if confirm_subscriber(&db_pool, subscriber_id).await.is_err() {
return HttpResponse::InternalServerError().finish();
}
HttpResponse::Ok().finish()
impl ResponseError for ConfirmationError {
fn status_code(&self) -> StatusCode {
match self {
Self::UnknownToken => StatusCode::UNAUTHORIZED,
Self::UnexpectedError(_) => StatusCode::INTERNAL_SERVER_ERROR,
}
}
}

#[tracing::instrument(name = "Confirm a pending subscriber", skip(params, db_pool))]
pub async fn confirm(
params: web::Query<Parameters>,
db_pool: web::Data<PgPool>,
) -> Result<HttpResponse, ConfirmationError> {
let id = get_subscriber_id_from_token(&db_pool, &params.subscription_token)
.await
.context("Failed to retrieve the subscriber id associated with the provided token.")?
.ok_or(ConfirmationError::UnknownToken)?;

confirm_subscriber(&db_pool, id)
.await
.context("Failed to update the subscriber status to `confirmed`.")?;

Ok(HttpResponse::Ok().finish())
}

#[tracing::instrument(name = "Mark subcriber as confirmed", skip(id, pool))]
pub async fn confirm_subscriber(pool: &PgPool, id: Uuid) -> Result<(), sqlx::Error> {
sqlx::query!(
Expand All @@ -36,14 +61,7 @@ pub async fn confirm_subscriber(pool: &PgPool, id: Uuid) -> Result<(), sqlx::Err
id
)
.execute(pool)
.await
.map_err(|e| {
tracing::error!(
"Failed to update subscriber status in the database: {:?}",
e
);
e
})?;
.await?;
Ok(())
}

Expand All @@ -61,10 +79,7 @@ async fn get_subscriber_id_from_token(
token,
)
.fetch_optional(pool)
.await
.map_err(|e| {
tracing::error!("Failed to execute query: {:?}", e);
e
})?;
.await?;

Ok(result.map(|r| r.subscriber_id))
}

0 comments on commit c6280d6

Please sign in to comment.