Skip to content

Commit 902f48e

Browse files
committed
migrate homepage, release-lists & search to axum
1 parent 117deef commit 902f48e

File tree

5 files changed

+375
-249
lines changed

5 files changed

+375
-249
lines changed

Cargo.toml

+2-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ clap = { version = "4.0.22", features = [ "derive" ] }
3333
crates-index = { version = "0.18.5", optional = true }
3434
rayon = { version = "1", optional = true }
3535
crates-index-diff = "13.0.2"
36-
reqwest = { version = "0.11", features = ["blocking", "json"] } # TODO: Remove blocking when async is ready
36+
reqwest = { version = "0.11", features = ["json"] }
3737
semver = { version = "1.0.4", features = ["serde"] }
3838
slug = "0.1.1"
3939
r2d2 = "0.8"
@@ -131,6 +131,7 @@ aws-smithy-client = { version = "0.51.0", features = ["test-util"]}
131131
aws-smithy-http = "0.51.0"
132132
tokio = { version = "1.0", features = ["rt-multi-thread", "macros"] }
133133
indoc = "1.0.7"
134+
reqwest = { version = "0.11", features = ["blocking", "json"] }
134135

135136
[build-dependencies]
136137
time = "0.3"

src/web/error.rs

+64-32
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 {
@@ -234,8 +215,59 @@ impl IntoResponse for AxumNope {
234215
}
235216
}
236217

218+
impl From<Nope> for AxumNope {
219+
fn from(err: Nope) -> Self {
220+
match err {
221+
Nope::ResourceNotFound => AxumNope::ResourceNotFound,
222+
Nope::BuildNotFound => AxumNope::BuildNotFound,
223+
Nope::CrateNotFound => AxumNope::CrateNotFound,
224+
Nope::OwnerNotFound => AxumNope::OwnerNotFound,
225+
Nope::VersionNotFound => AxumNope::VersionNotFound,
226+
Nope::InternalServerError => AxumNope::InternalServerError,
227+
}
228+
}
229+
}
230+
237231
pub(crate) type AxumResult<T> = Result<T, AxumNope>;
238232

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

src/web/mod.rs

+77-3
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,12 +92,14 @@ 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,
9999
http::{uri::Authority, StatusCode},
100-
middleware, Router as AxumRouter,
100+
middleware,
101+
response::IntoResponse,
102+
Router as AxumRouter,
101103
};
102104
use chrono::{DateTime, Utc};
103105
use csp::CspMiddleware;
@@ -121,6 +123,7 @@ use std::{borrow::Cow, net::SocketAddr, sync::Arc};
121123
use strangler::StranglerService;
122124
use tower::ServiceBuilder;
123125
use tower_http::trace::TraceLayer;
126+
use url::form_urlencoded;
124127

125128
/// Duration of static files for staticfile and DatabaseFileHandler (in seconds)
126129
const STATIC_FILE_CACHE_DURATION: u64 = 60 * 60 * 24 * 30 * 12; // 12 months
@@ -426,6 +429,27 @@ fn match_version(
426429
Err(Nope::VersionNotFound)
427430
}
428431

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+
429453
#[instrument(skip_all)]
430454
pub(crate) fn build_axum_app(
431455
context: &dyn Context,
@@ -530,6 +554,33 @@ fn redirect(url: Url) -> Response {
530554
resp
531555
}
532556

557+
fn axum_redirect<U>(uri: U) -> Result<impl IntoResponse, Error>
558+
where
559+
U: TryInto<http::Uri>,
560+
<U as TryInto<http::Uri>>::Error: std::fmt::Debug,
561+
{
562+
let uri: http::Uri = uri
563+
.try_into()
564+
.map_err(|err| anyhow!("invalid URI: {:?}", err))?;
565+
566+
if let Some(path_and_query) = uri.path_and_query() {
567+
if path_and_query.as_str().starts_with("//") {
568+
bail!("protocol relative redirects are forbidden");
569+
}
570+
} else {
571+
// we always want a path to redirect to, even when it's just `/`
572+
bail!("missing path in URI");
573+
}
574+
575+
Ok((
576+
StatusCode::FOUND,
577+
[(
578+
http::header::LOCATION,
579+
http::HeaderValue::try_from(uri.to_string()).context("invalid uri for redirect")?,
580+
)],
581+
))
582+
}
583+
533584
fn cached_redirect(url: Url, cache_policy: cache::CachePolicy) -> Response {
534585
let mut resp = Response::with((status::Found, Redirect(url)));
535586
resp.extensions.insert::<cache::CachePolicy>(cache_policy);
@@ -574,6 +625,29 @@ where
574625
}
575626
}
576627

628+
/// Parse an URI into a http::Uri struct.
629+
/// When `queries` are given these are added to the URL,
630+
/// with empty `queries` the `?` will be omitted.
631+
pub(crate) fn axum_parse_uri_with_params<I, K, V>(uri: &str, queries: I) -> Result<http::Uri, Error>
632+
where
633+
I: IntoIterator,
634+
I::Item: Borrow<(K, V)>,
635+
K: AsRef<str>,
636+
V: AsRef<str>,
637+
{
638+
let mut queries = queries.into_iter().peekable();
639+
if queries.peek().is_some() {
640+
let query_params: String = form_urlencoded::Serializer::new(String::new())
641+
.extend_pairs(queries)
642+
.finish();
643+
format!("{uri}?{}", query_params)
644+
.parse::<http::Uri>()
645+
.context("error parsing URL")
646+
} else {
647+
uri.parse::<http::Uri>().context("error parsing URL")
648+
}
649+
}
650+
577651
/// MetaData used in header
578652
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
579653
pub(crate) struct MetaData {

0 commit comments

Comments
 (0)