Skip to content

Commit 0eb570d

Browse files
committed
migrate crate-details handler to axum
1 parent 83883be commit 0eb570d

File tree

5 files changed

+177
-73
lines changed

5 files changed

+177
-73
lines changed

src/utils/mod.rs

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,9 +107,27 @@ where
107107
F: FnOnce() -> Result<R> + Send + 'static,
108108
R: Send + 'static,
109109
{
110-
tokio::task::spawn_blocking(f)
110+
let result = tokio::task::spawn_blocking(f)
111111
.await
112-
.context("failed to join thread")?
112+
.context("failed to join thread")?;
113+
114+
if cfg!(debug_assertions) {
115+
// Since `AxumNope` can also be silently converted into
116+
// `anyhow::Error`, someone accidentally using this method
117+
// instead of `web::error::spawn_blocking_web` can lead to
118+
// a more specific AxumNope error with a specific status code
119+
// being silently converted into an anyhow::Error, so a server
120+
// error with status 500.
121+
if let Err(ref err) = result {
122+
use crate::web::error::AxumNope;
123+
assert!(
124+
err.downcast_ref::<AxumNope>().is_none(),
125+
"AxumNope found inside anyhow::Error, this is likely a problem."
126+
);
127+
}
128+
}
129+
130+
result
113131
}
114132

115133
#[cfg(test)]

src/web/crate_details.rs

Lines changed: 74 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,26 @@
1-
use super::{markdown, match_version, redirect_base, MatchSemver, MetaData};
2-
use crate::utils::{get_correct_docsrs_style_file, report_error};
1+
use super::error::spawn_blocking_web;
2+
use super::{markdown, match_version, MatchSemver, MetaData};
3+
use crate::utils::{get_correct_docsrs_style_file, report_error, spawn_blocking};
34
use crate::{
45
db::Pool,
5-
impl_webpage,
6+
impl_axum_webpage,
67
repositories::RepositoryStatsUpdater,
7-
web::{cache::CachePolicy, page::WebPage},
8+
web::{
9+
cache::CachePolicy,
10+
error::{AxumNope, AxumResult},
11+
},
12+
};
13+
use anyhow::{anyhow, Context as _};
14+
use axum::{
15+
extract::{Extension, Path},
16+
response::{IntoResponse, Response as AxumResponse},
817
};
9-
use anyhow::anyhow;
1018
use chrono::{DateTime, Utc};
11-
use iron::prelude::*;
12-
use iron::Url;
1319
use postgres::GenericClient;
14-
use router::Router;
20+
use serde::Deserialize;
1521
use serde::{ser::Serializer, Serialize};
1622
use serde_json::Value;
23+
use std::sync::Arc;
1724

1825
// TODO: Add target name and versions
1926

@@ -293,75 +300,85 @@ struct CrateDetailsPage {
293300
details: CrateDetails,
294301
}
295302

296-
impl_webpage! {
303+
impl_axum_webpage! {
297304
CrateDetailsPage = "crate/details.html",
298305
}
299306

300-
pub fn crate_details_handler(req: &mut Request) -> IronResult<Response> {
301-
let router = extension!(req, Router);
302-
// this handler must always called with a crate name
303-
let name = cexpect!(req, router.find("name"));
304-
let req_version = router.find("version");
307+
#[derive(Deserialize, Clone, Debug)]
308+
pub(crate) struct CrateDetailHandlerParams {
309+
name: String,
310+
version: Option<String>,
311+
}
305312

306-
if req_version.is_none() {
307-
let url = ctry!(
308-
req,
309-
Url::parse(&format!("{}/crate/{}/latest", redirect_base(req), name,)),
310-
);
311-
return Ok(super::cached_redirect(url, CachePolicy::ForeverInCdn));
313+
#[tracing::instrument]
314+
pub(crate) async fn crate_details_handler(
315+
Path(params): Path<CrateDetailHandlerParams>,
316+
Extension(pool): Extension<Pool>,
317+
Extension(repository_stats_updater): Extension<Arc<RepositoryStatsUpdater>>,
318+
) -> AxumResult<AxumResponse> {
319+
// this handler must always called with a crate name
320+
if params.version.is_none() {
321+
return Ok(super::axum_cached_redirect(
322+
&format!("/crate/{}/latest", params.name),
323+
CachePolicy::ForeverInCdn,
324+
)
325+
.into_response());
312326
}
313327

314-
let mut conn = extension!(req, Pool).get()?;
328+
let found_version = spawn_blocking_web({
329+
let pool = pool.clone();
330+
let params = params.clone();
331+
move || {
332+
let mut conn = pool.get().context("could not get connection from pool")?;
333+
match_version(&mut conn, &params.name, params.version.as_deref())
334+
.and_then(|m| m.assume_exact())
335+
.map_err(Into::<AxumNope>::into)
336+
}
337+
})
338+
.await?;
315339

316-
let found_version =
317-
match_version(&mut conn, name, req_version).and_then(|m| m.assume_exact())?;
318340
let (version, version_or_latest, is_latest_url) = match found_version {
319341
MatchSemver::Exact((version, _)) => (version.clone(), version, false),
320342
MatchSemver::Latest((version, _)) => (version, "latest".to_string(), true),
321343
MatchSemver::Semver((version, _)) => {
322-
let url = ctry!(
323-
req,
324-
Url::parse(&format!(
325-
"{}/crate/{}/{}",
326-
redirect_base(req),
327-
name,
328-
version
329-
)),
330-
);
331-
332-
return Ok(super::cached_redirect(url, CachePolicy::ForeverInCdn));
344+
return Ok(super::axum_cached_redirect(
345+
&format!("/crate/{}/{}", &params.name, version),
346+
CachePolicy::ForeverInCdn,
347+
)
348+
.into_response());
333349
}
334350
};
335351

336-
let updater = extension!(req, RepositoryStatsUpdater);
337-
let details = cexpect!(
338-
req,
339-
ctry!(
340-
req,
341-
CrateDetails::new(
342-
&mut *conn,
343-
name,
344-
&version,
345-
&version_or_latest,
346-
Some(updater)
347-
)
352+
let details = spawn_blocking(move || {
353+
let mut conn = pool.get()?;
354+
CrateDetails::new(
355+
&mut *conn,
356+
&params.name,
357+
&version,
358+
&version_or_latest,
359+
Some(&repository_stats_updater),
348360
)
349-
);
350-
351-
let mut res = CrateDetailsPage { details }.into_response(req)?;
352-
res.extensions.insert::<CachePolicy>(if is_latest_url {
353-
CachePolicy::ForeverInCdn
354-
} else {
355-
CachePolicy::ForeverInCdnAndStaleInBrowser
356-
});
357-
Ok(res)
361+
})
362+
.await?
363+
.ok_or(AxumNope::VersionNotFound)?;
364+
365+
let mut res = CrateDetailsPage { details }.into_response();
366+
res.extensions_mut()
367+
.insert::<CachePolicy>(if is_latest_url {
368+
CachePolicy::ForeverInCdn
369+
} else {
370+
CachePolicy::ForeverInCdnAndStaleInBrowser
371+
});
372+
Ok(res.into_response())
358373
}
359374

360375
#[cfg(test)]
361376
mod tests {
362377
use super::*;
363378
use crate::index::api::CrateOwner;
364-
use crate::test::{assert_cache_control, assert_redirect_cached, wrapper, TestDatabase};
379+
use crate::test::{
380+
assert_cache_control, assert_redirect, assert_redirect_cached, wrapper, TestDatabase,
381+
};
365382
use anyhow::{Context, Error};
366383
use kuchiki::traits::TendrilSink;
367384
use std::collections::HashMap;
@@ -1035,13 +1052,7 @@ mod tests {
10351052
assert!(body.contains("<a href=\"/crate/dummy/latest/source/\""));
10361053
assert!(body.contains("<a href=\"/crate/dummy/latest\""));
10371054

1038-
assert_redirect_cached(
1039-
"/crate/dummy/latest/",
1040-
"/crate/dummy/latest",
1041-
CachePolicy::NoCaching,
1042-
web,
1043-
&env.config(),
1044-
)?;
1055+
assert_redirect("/crate/dummy/latest/", "/crate/dummy/latest", web)?;
10451056
assert_redirect_cached(
10461057
"/crate/dummy",
10471058
"/crate/dummy/latest",

src/web/error.rs

Lines changed: 53 additions & 0 deletions
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},
@@ -234,8 +235,60 @@ impl IntoResponse for AxumNope {
234235
}
235236
}
236237

238+
impl From<Nope> for AxumNope {
239+
fn from(err: Nope) -> Self {
240+
match err {
241+
Nope::ResourceNotFound => AxumNope::ResourceNotFound,
242+
Nope::BuildNotFound => AxumNope::BuildNotFound,
243+
Nope::CrateNotFound => AxumNope::CrateNotFound,
244+
Nope::OwnerNotFound => AxumNope::OwnerNotFound,
245+
Nope::VersionNotFound => AxumNope::VersionNotFound,
246+
Nope::NoResults => todo!(),
247+
Nope::InternalServerError => AxumNope::InternalServerError,
248+
}
249+
}
250+
}
251+
237252
pub(crate) type AxumResult<T> = Result<T, AxumNope>;
238253

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

src/web/mod.rs

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ mod builds;
7777
pub(crate) mod cache;
7878
pub(crate) mod crate_details;
7979
mod csp;
80-
mod error;
80+
pub(crate) mod error;
8181
mod extensions;
8282
mod features;
8383
mod file;
@@ -97,7 +97,9 @@ 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;
@@ -327,7 +329,7 @@ fn match_version(
327329
WHERE normalize_crate_name(name) = normalize_crate_name($1)",
328330
&[&name],
329331
)
330-
.unwrap();
332+
.unwrap(); // FIXME: remove this unwrap when all handlers using it are migrated to axum
331333

332334
let row = rows.get(0).ok_or(Nope::CrateNotFound)?;
333335

@@ -443,6 +445,7 @@ pub(crate) fn build_axum_app(
443445
.layer(Extension(context.metrics()?))
444446
.layer(Extension(context.config()?))
445447
.layer(Extension(context.storage()?))
448+
.layer(Extension(context.repository_stats_updater()?))
446449
.layer(Extension(template_data))
447450
.layer(middleware::from_fn(csp::csp_middleware))
448451
.layer(middleware::from_fn(
@@ -536,12 +539,28 @@ fn redirect(url: Url) -> Response {
536539
resp
537540
}
538541

542+
fn axum_redirect(url: &str) -> impl IntoResponse {
543+
(
544+
StatusCode::FOUND,
545+
[(
546+
http::header::LOCATION,
547+
http::HeaderValue::try_from(url).expect("invalid url for redirect"),
548+
)],
549+
)
550+
}
551+
539552
fn cached_redirect(url: Url, cache_policy: cache::CachePolicy) -> Response {
540553
let mut resp = Response::with((status::Found, Redirect(url)));
541554
resp.extensions.insert::<cache::CachePolicy>(cache_policy);
542555
resp
543556
}
544557

558+
fn axum_cached_redirect(url: &str, cache_policy: cache::CachePolicy) -> impl IntoResponse {
559+
let mut resp = axum_redirect(url).into_response();
560+
resp.extensions_mut().insert(cache_policy);
561+
resp
562+
}
563+
545564
fn redirect_base(req: &Request) -> String {
546565
// Try to get the scheme from CloudFront first, and then from iron
547566
let scheme = req

src/web/routes.rs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,14 @@ pub(super) fn build_axum_routes() -> AxumRouter {
105105
"/releases/failures/:page",
106106
get_internal(super::releases::releases_failures_by_stars_handler),
107107
)
108+
.route(
109+
"/crate/:name",
110+
get_internal(super::crate_details::crate_details_handler),
111+
)
112+
.route(
113+
"/crate/:name/:version",
114+
get_internal(super::crate_details::crate_details_handler),
115+
)
108116
}
109117

110118
// REFACTOR: Break this into smaller initialization functions
@@ -152,11 +160,6 @@ pub(super) fn build_routes() -> Routes {
152160
routes.internal_page("/releases/search", super::releases::search_handler);
153161
routes.internal_page("/releases/queue", super::releases::build_queue_handler);
154162

155-
routes.internal_page("/crate/:name", super::crate_details::crate_details_handler);
156-
routes.internal_page(
157-
"/crate/:name/:version",
158-
super::crate_details::crate_details_handler,
159-
);
160163
routes.internal_page(
161164
"/crate/:name/:version/builds",
162165
super::builds::build_list_handler,

0 commit comments

Comments
 (0)