Skip to content

Commit ecac377

Browse files
committed
migrate homepage, release-lists & search to axum
1 parent cae6880 commit ecac377

File tree

4 files changed

+348
-252
lines changed

4 files changed

+348
-252
lines changed

src/web/error.rs

+51-33
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use crate::{
44
db::PoolError,
55
web::{page::WebPage, releases::Search, AxumErrorPage, ErrorPage},
66
};
7+
use anyhow::Context as _;
78
use axum::{
89
http::StatusCode,
910
response::{IntoResponse, Response as AxumResponse},
@@ -22,8 +23,6 @@ pub enum Nope {
2223
OwnerNotFound,
2324
#[error("Requested crate does not have specified version")]
2425
VersionNotFound,
25-
#[error("Search yielded no results")]
26-
NoResults,
2726
#[error("Internal server error")]
2827
InternalServerError,
2928
}
@@ -37,8 +36,7 @@ impl From<Nope> for IronError {
3736
| Nope::BuildNotFound
3837
| Nope::CrateNotFound
3938
| Nope::OwnerNotFound
40-
| Nope::VersionNotFound
41-
| Nope::NoResults => status::NotFound,
39+
| Nope::VersionNotFound => status::NotFound,
4240
Nope::InternalServerError => status::InternalServerError,
4341
};
4442

@@ -96,29 +94,6 @@ impl Handler for Nope {
9694
.into_response(req)
9795
}
9896

99-
Nope::NoResults => {
100-
let mut params = req.url.as_ref().query_pairs();
101-
102-
if let Some((_, query)) = params.find(|(key, _)| key == "query") {
103-
// this used to be a search
104-
Search {
105-
title: format!("No crates found matching '{}'", query),
106-
search_query: Some(query.into_owned()),
107-
status: Status::NotFound,
108-
..Default::default()
109-
}
110-
.into_response(req)
111-
} else {
112-
// user did a search with no search terms
113-
Search {
114-
title: "No results given for empty search query".to_owned(),
115-
status: Status::NotFound,
116-
..Default::default()
117-
}
118-
.into_response(req)
119-
}
120-
}
121-
12297
Nope::InternalServerError => {
12398
// something went wrong, details should have been logged
12499
ErrorPage {
@@ -151,8 +126,8 @@ pub enum AxumNope {
151126
OwnerNotFound,
152127
#[error("Requested crate does not have specified version")]
153128
VersionNotFound,
154-
// #[error("Search yielded no results")]
155-
// NoResults,
129+
#[error("Search yielded no results")]
130+
NoResults,
156131
#[error("Internal server error")]
157132
InternalServerError,
158133
#[error("internal error")]
@@ -207,9 +182,15 @@ impl IntoResponse for AxumNope {
207182
}
208183
.into_response()
209184
}
210-
// AxumNope::NoResults => {
211-
// todo!("to be implemented when search-handler is migrated to axum")
212-
// }
185+
AxumNope::NoResults => {
186+
// user did a search with no search terms
187+
Search {
188+
title: "No results given for empty search query".to_owned(),
189+
status: StatusCode::NOT_FOUND,
190+
..Default::default()
191+
}
192+
.into_response()
193+
}
213194
AxumNope::InternalServerError => {
214195
// something went wrong, details should have been logged
215196
AxumErrorPage {
@@ -254,14 +235,51 @@ impl From<Nope> for AxumNope {
254235
Nope::CrateNotFound => AxumNope::CrateNotFound,
255236
Nope::OwnerNotFound => AxumNope::OwnerNotFound,
256237
Nope::VersionNotFound => AxumNope::VersionNotFound,
257-
Nope::NoResults => todo!(),
258238
Nope::InternalServerError => AxumNope::InternalServerError,
259239
}
260240
}
261241
}
262242

263243
pub(crate) type AxumResult<T> = Result<T, AxumNope>;
264244

245+
/// spawn-blocking helper for usage in axum requests.
246+
///
247+
/// Should be used when spawning tasks from inside web-handlers
248+
/// that return AxumNope as error. The join-error will also be
249+
/// converted into an `anyhow::Error`. Any standard AxumNope
250+
/// will be left intact so the error-handling can generate the
251+
/// correct status-page with the correct status code.
252+
///
253+
/// Examples:
254+
///
255+
/// with standard `tokio::task::spawn_blocking`:
256+
/// ```ignore
257+
/// let data = spawn_blocking(move || -> Result<_, AxumNope> {
258+
/// let data = get_the_data()?;
259+
/// Ok(data)
260+
/// })
261+
/// .await
262+
/// .context("failed to join thread")??;
263+
/// ```
264+
///
265+
/// with this helper function:
266+
/// ```ignore
267+
/// let data = spawn_blocking(move || {
268+
/// let data = get_the_data()?;
269+
/// Ok(data)
270+
/// })
271+
/// .await?
272+
/// ```
273+
pub(crate) async fn spawn_blocking_web<F, R>(f: F) -> AxumResult<R>
274+
where
275+
F: FnOnce() -> AxumResult<R> + Send + 'static,
276+
R: Send + 'static,
277+
{
278+
tokio::task::spawn_blocking(f)
279+
.await
280+
.context("failed to join thread")?
281+
}
282+
265283
#[cfg(test)]
266284
mod tests {
267285
use crate::test::wrapper;

src/web/mod.rs

+65-6
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ pub mod page;
44

55
use crate::utils::get_correct_docsrs_style_file;
66
use crate::utils::report_error;
7-
use anyhow::anyhow;
7+
use anyhow::{anyhow, bail, Context as _};
88
use serde_json::Value;
99
use tracing::{info, instrument};
1010

@@ -92,7 +92,7 @@ mod source;
9292
mod statics;
9393
mod strangler;
9494

95-
use crate::{impl_axum_webpage, impl_webpage, Context};
95+
use crate::{db::Pool, impl_axum_webpage, impl_webpage, Context};
9696
use anyhow::Error;
9797
use axum::{
9898
extract::Extension,
@@ -123,6 +123,7 @@ use std::{borrow::Cow, net::SocketAddr, sync::Arc};
123123
use strangler::StranglerService;
124124
use tower::ServiceBuilder;
125125
use tower_http::trace::TraceLayer;
126+
use url::form_urlencoded;
126127

127128
/// Duration of static files for staticfile and DatabaseFileHandler (in seconds)
128129
const STATIC_FILE_CACHE_DURATION: u64 = 60 * 60 * 24 * 30 * 12; // 12 months
@@ -428,6 +429,27 @@ fn match_version(
428429
Err(Nope::VersionNotFound)
429430
}
430431

432+
// temporary wrapper around `match_version` for axum handlers.
433+
//
434+
// FIXME: this can go when we fully migrated to axum / async in web
435+
async fn match_version_axum(
436+
pool: &Pool,
437+
name: &str,
438+
input_version: Option<&str>,
439+
) -> Result<MatchVersion, error::AxumNope> {
440+
error::spawn_blocking_web({
441+
let name = name.to_owned();
442+
let input_version = input_version.map(str::to_owned);
443+
let pool = pool.clone();
444+
move || {
445+
let mut conn = pool.get().context("could not get connection from pool")?;
446+
match_version(&mut conn, &name, input_version.as_deref())
447+
.map_err(Into::<error::AxumNope>::into)
448+
}
449+
})
450+
.await
451+
}
452+
431453
#[instrument(skip_all)]
432454
pub(crate) fn build_axum_app(
433455
context: &dyn Context,
@@ -539,15 +561,29 @@ fn redirect(url: Url) -> Response {
539561
resp
540562
}
541563

542-
fn axum_redirect(url: &str) -> Result<impl IntoResponse, Error> {
543-
if !url.starts_with('/') || url.starts_with("//") {
544-
return Err(anyhow!("invalid redirect URL: {}", url));
564+
fn axum_redirect<U>(uri: U) -> Result<impl IntoResponse, Error>
565+
where
566+
U: TryInto<http::Uri>,
567+
<U as TryInto<http::Uri>>::Error: std::fmt::Debug,
568+
{
569+
let uri: http::Uri = uri
570+
.try_into()
571+
.map_err(|err| anyhow!("invalid URI: {:?}", err))?;
572+
573+
if let Some(path_and_query) = uri.path_and_query() {
574+
if path_and_query.as_str().starts_with("//") {
575+
bail!("protocol relative redirects are forbidden");
576+
}
577+
} else {
578+
// we always want a path to redirect to, even when it's just `/`
579+
bail!("missing path in URI");
545580
}
581+
546582
Ok((
547583
StatusCode::FOUND,
548584
[(
549585
http::header::LOCATION,
550-
http::HeaderValue::try_from(url).expect("invalid url for redirect"),
586+
http::HeaderValue::try_from(uri.to_string()).context("invalid uri for redirect")?,
551587
)],
552588
))
553589
}
@@ -605,6 +641,29 @@ where
605641
}
606642
}
607643

644+
/// Parse an URI into a http::Uri struct.
645+
/// When `queries` are given these are added to the URL,
646+
/// with empty `queries` the `?` will be omitted.
647+
pub(crate) fn axum_parse_uri_with_params<I, K, V>(uri: &str, queries: I) -> Result<http::Uri, Error>
648+
where
649+
I: IntoIterator,
650+
I::Item: Borrow<(K, V)>,
651+
K: AsRef<str>,
652+
V: AsRef<str>,
653+
{
654+
let mut queries = queries.into_iter().peekable();
655+
if queries.peek().is_some() {
656+
let query_params: String = form_urlencoded::Serializer::new(String::new())
657+
.extend_pairs(queries)
658+
.finish();
659+
format!("{uri}?{}", query_params)
660+
.parse::<http::Uri>()
661+
.context("error parsing URL")
662+
} else {
663+
uri.parse::<http::Uri>().context("error parsing URL")
664+
}
665+
}
666+
608667
/// MetaData used in header
609668
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
610669
pub(crate) struct MetaData {

0 commit comments

Comments
 (0)