From f43e143bf6002b97ab75301b70201008f40774de Mon Sep 17 00:00:00 2001 From: Alphonse Paix Date: Sat, 27 Sep 2025 02:28:04 +0200 Subject: [PATCH] custom extractors rejection --- src/routes.rs | 55 +++++++++++++++++++++++++++-- src/routes/admin/posts.rs | 6 ++-- src/routes/admin/subscribers.rs | 4 +-- src/routes/posts.rs | 6 ++-- src/routes/subscriptions_confirm.rs | 8 +++-- templates/unsubscribe.html | 2 +- 6 files changed, 68 insertions(+), 13 deletions(-) diff --git a/src/routes.rs b/src/routes.rs index b269bd3..474c00f 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -10,7 +10,8 @@ mod unsubscribe; pub use admin::*; use askama::Template; use axum::{ - http::HeaderMap, + extract::FromRequestParts, + http::{HeaderMap, request::Parts}, response::{Html, IntoResponse, Response}, }; pub use health_check::*; @@ -19,6 +20,7 @@ pub use login::*; pub use posts::*; use rand::{Rng, distr::Alphanumeric}; use reqwest::StatusCode; +use serde::de::DeserializeOwned; pub use subscriptions::*; pub use subscriptions_confirm::*; pub use unsubscribe::*; @@ -61,6 +63,8 @@ pub enum AppError { FormError(#[source] anyhow::Error), #[error("Authentication is required.")] NotAuthenticated, + #[error("Handler extractor failed.")] + Extractor(#[source] anyhow::Error), } impl From for AppError { @@ -124,6 +128,7 @@ impl IntoResponse for AppError { headers.insert("HX-Redirect", "/login".parse().unwrap()); (StatusCode::OK, headers).into_response() } + AppError::Extractor(_) => not_found_html(), } } } @@ -155,6 +160,50 @@ impl From for AppError { } pub async fn not_found() -> Response { - let template = HtmlTemplate(NotFoundTemplate); - (StatusCode::NOT_FOUND, template).into_response() + (StatusCode::NOT_FOUND, not_found_html()).into_response() +} + +pub fn not_found_html() -> Response { + let template = HtmlTemplate(NotFoundTemplate); + template.into_response() +} + +pub struct Path(T); + +impl FromRequestParts for Path +where + T: DeserializeOwned + Send, + S: Send + Sync, +{ + type Rejection = AppError; + + async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { + match axum::extract::Path::::from_request_parts(parts, state).await { + Ok(value) => Ok(Self(value.0)), + Err(rejection) => Err(AppError::Extractor(anyhow::anyhow!( + "Path rejection: {:?}", + rejection + ))), + } + } +} + +pub struct Query(pub T); + +impl FromRequestParts for Query +where + T: DeserializeOwned, + S: Send + Sync, +{ + type Rejection = AppError; + + async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { + match axum::extract::Query::::from_request_parts(parts, state).await { + Ok(value) => Ok(Self(value.0)), + Err(rejection) => Err(AppError::Extractor(anyhow::anyhow!( + "Query rejection: {:?}", + rejection + ))), + } + } } diff --git a/src/routes/admin/posts.rs b/src/routes/admin/posts.rs index a7f676d..b18aca5 100644 --- a/src/routes/admin/posts.rs +++ b/src/routes/admin/posts.rs @@ -1,7 +1,9 @@ use crate::{ authentication::AuthenticatedUser, idempotency::{IdempotencyKey, save_response, try_processing}, - routes::{AdminError, AppError, EmailType, enqueue_delivery_tasks, insert_newsletter_issue}, + routes::{ + AdminError, AppError, EmailType, Path, enqueue_delivery_tasks, insert_newsletter_issue, + }, startup::AppState, templates::{MessageTemplate, NewPostEmailTemplate}, }; @@ -9,7 +11,7 @@ use anyhow::Context; use askama::Template; use axum::{ Extension, Form, - extract::{Path, State}, + extract::State, response::{Html, IntoResponse, Response}, }; use chrono::Utc; diff --git a/src/routes/admin/subscribers.rs b/src/routes/admin/subscribers.rs index 663ddf8..9c26d1f 100644 --- a/src/routes/admin/subscribers.rs +++ b/src/routes/admin/subscribers.rs @@ -1,13 +1,13 @@ use crate::{ domain::SubscriberEntry, - routes::AppError, + routes::{AppError, Path, Query}, startup::AppState, templates::{MessageTemplate, SubListTemplate}, }; use anyhow::Context; use askama::Template; use axum::{ - extract::{Path, Query, State}, + extract::State, response::{Html, IntoResponse, Response}, }; use sqlx::PgPool; diff --git a/src/routes/posts.rs b/src/routes/posts.rs index 422fc92..f001ec8 100644 --- a/src/routes/posts.rs +++ b/src/routes/posts.rs @@ -1,13 +1,13 @@ use crate::{ domain::PostEntry, - routes::{AppError, not_found}, + routes::{AppError, Path, Query, not_found_html}, startup::AppState, templates::{HtmlTemplate, PostListTemplate, PostTemplate, PostsTemplate}, }; use anyhow::Context; use askama::Template; use axum::{ - extract::{Path, Query, State}, + extract::State, response::{Html, IntoResponse, Redirect, Response}, }; use sqlx::PgPool; @@ -90,7 +90,7 @@ pub async fn see_post( let template = HtmlTemplate(PostTemplate { post }); Ok(template.into_response()) } else { - Ok(not_found().await) + Ok(not_found_html()) } } diff --git a/src/routes/subscriptions_confirm.rs b/src/routes/subscriptions_confirm.rs index ec6942b..c45a208 100644 --- a/src/routes/subscriptions_confirm.rs +++ b/src/routes/subscriptions_confirm.rs @@ -1,7 +1,11 @@ -use crate::{routes::generate_token, startup::AppState, templates::ConfirmTemplate}; +use crate::{ + routes::{Query, generate_token}, + startup::AppState, + templates::ConfirmTemplate, +}; use askama::Template; use axum::{ - extract::{Query, State}, + extract::State, http::StatusCode, response::{Html, IntoResponse, Response}, }; diff --git a/templates/unsubscribe.html b/templates/unsubscribe.html index 4bdcf21..0c6f3a6 100644 --- a/templates/unsubscribe.html +++ b/templates/unsubscribe.html @@ -39,7 +39,7 @@
-

You will receive an email with an confirmation link.

+

You will receive an email with a confirmation link.