| 10 |
10 |
|
//! friendly template for page routes, `{"error": "..."}` for API routes (via
|
| 11 |
11 |
|
//! `json_error_layer`), and a 422.
|
| 12 |
12 |
|
|
| 13 |
|
- |
use axum::extract::{FromRequest, Request};
|
|
13 |
+ |
use axum::extract::{FromRequest, FromRequestParts, Request};
|
|
14 |
+ |
use axum::http::request::Parts;
|
| 14 |
15 |
|
use serde::de::DeserializeOwned;
|
| 15 |
16 |
|
|
| 16 |
17 |
|
use crate::error::AppError;
|
| 73 |
74 |
|
}
|
| 74 |
75 |
|
}
|
| 75 |
76 |
|
|
|
77 |
+ |
/// `axum::extract::Query<T>` whose extraction failures become
|
|
78 |
+ |
/// [`AppError::BadRequest`] instead of axum's bare `text/plain` rejection.
|
|
79 |
+ |
///
|
|
80 |
+ |
/// Query strings are the one input a visitor edits by hand or inherits from a
|
|
81 |
+ |
/// stale link, so a malformed one is the most likely way to meet an error page
|
|
82 |
+ |
/// at all. Left as a raw `Query<T>` it answers with unlayouted framework text
|
|
83 |
+ |
/// (loose-wire g2-06); routed through here it gets the same branded template,
|
|
84 |
+ |
/// `HX-Error` header and JSON-on-API-routes treatment as every other failure.
|
|
85 |
+ |
///
|
|
86 |
+ |
/// 400 rather than the 422 the body extractors use: nothing was submitted for
|
|
87 |
+ |
/// the server to process, the address itself is wrong.
|
|
88 |
+ |
pub struct ValidatedQuery<T>(pub T);
|
|
89 |
+ |
|
|
90 |
+ |
impl<T, S> FromRequestParts<S> for ValidatedQuery<T>
|
|
91 |
+ |
where
|
|
92 |
+ |
T: DeserializeOwned,
|
|
93 |
+ |
S: Send + Sync,
|
|
94 |
+ |
{
|
|
95 |
+ |
type Rejection = AppError;
|
|
96 |
+ |
|
|
97 |
+ |
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
|
|
98 |
+ |
match axum::extract::Query::<T>::from_request_parts(parts, state).await {
|
|
99 |
+ |
Ok(axum::extract::Query(value)) => Ok(Self(value)),
|
|
100 |
+ |
Err(rejection) => Err(rejection_to_bad_request(&rejection.body_text())),
|
|
101 |
+ |
}
|
|
102 |
+ |
}
|
|
103 |
+ |
}
|
|
104 |
+ |
|
|
105 |
+ |
/// `axum_extra::extract::Query<T>` (repeated-param query) with the same
|
|
106 |
+ |
/// treatment as [`ValidatedQuery`].
|
|
107 |
+ |
///
|
|
108 |
+ |
/// Separate from `ValidatedQuery` for the same reason `ValidatedHtmlForm` is
|
|
109 |
+ |
/// separate from `ValidatedForm`: only the `axum_extra` extractor collects
|
|
110 |
+ |
/// `?tag=a&tag=b` into a `Vec`, and swapping one for the other silently turns
|
|
111 |
+ |
/// every multi-select filter on the discover page into a 400.
|
|
112 |
+ |
pub struct ValidatedExtraQuery<T>(pub T);
|
|
113 |
+ |
|
|
114 |
+ |
impl<T, S> FromRequestParts<S> for ValidatedExtraQuery<T>
|
|
115 |
+ |
where
|
|
116 |
+ |
T: DeserializeOwned,
|
|
117 |
+ |
S: Send + Sync,
|
|
118 |
+ |
{
|
|
119 |
+ |
type Rejection = AppError;
|
|
120 |
+ |
|
|
121 |
+ |
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
|
|
122 |
+ |
match axum_extra::extract::Query::<T>::from_request_parts(parts, state).await {
|
|
123 |
+ |
Ok(axum_extra::extract::Query(value)) => Ok(Self(value)),
|
|
124 |
+ |
Err(rejection) => Err(rejection_to_bad_request(&rejection.to_string())),
|
|
125 |
+ |
}
|
|
126 |
+ |
}
|
|
127 |
+ |
}
|
|
128 |
+ |
|
|
129 |
+ |
/// Same prefix-stripping as [`rejection_to_validation`], for the query-string
|
|
130 |
+ |
/// wording axum uses, then wrapped as a 400.
|
|
131 |
+ |
fn rejection_to_bad_request(detail: &str) -> AppError {
|
|
132 |
+ |
let msg = detail
|
|
133 |
+ |
.strip_prefix("Failed to deserialize query string: ")
|
|
134 |
+ |
.unwrap_or(detail)
|
|
135 |
+ |
.trim();
|
|
136 |
+ |
let msg = if msg.is_empty() {
|
|
137 |
+ |
"That link has a bad address. Please check it and try again.".to_string()
|
|
138 |
+ |
} else {
|
|
139 |
+ |
format!("That link has a bad address: {msg}")
|
|
140 |
+ |
};
|
|
141 |
+ |
AppError::BadRequest(msg)
|
|
142 |
+ |
}
|
|
143 |
+ |
|
| 76 |
144 |
|
/// Strip axum's "Failed to deserialize ...: " machinery so the user-facing message
|
| 77 |
145 |
|
/// is the underlying cause (which, for a validating newtype, is its own error
|
| 78 |
146 |
|
/// string). Falls back to a generic message when nothing useful remains.
|