Skip to content

Commit 66a3ee4

Browse files
committed
Introduced Nope::VersionNotFound error
Trying to reproduce the issue rust-lang#55 revealed another bug: the order in which the different handlers were called in CratesfyiHandler::handle resulted in the more specific errors from the router_handler being dropped, due to them being 404 and the last handler being the static_handler which would just return a ResourceNotFound error. It should be noted that the different execution order might affect the performance in production.
1 parent bfa6ada commit 66a3ee4

File tree

5 files changed

+84
-47
lines changed

5 files changed

+84
-47
lines changed

src/web/crate_details.rs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
use super::error::Nope;
21
use super::{match_version, redirect_base, render_markdown, MatchSemver, MetaData};
32
use crate::{db::Pool, impl_webpage, web::page::WebPage};
43
use chrono::{DateTime, NaiveDateTime, Utc};
@@ -297,13 +296,13 @@ pub fn crate_details_handler(req: &mut Request) -> IronResult<Response> {
297296
let mut conn = extension!(req, Pool).get()?;
298297

299298
match match_version(&mut conn, &name, req_version).and_then(|m| m.assume_exact()) {
300-
Some(MatchSemver::Exact((version, _))) => {
299+
Ok(MatchSemver::Exact((version, _))) => {
301300
let details = cexpect!(req, CrateDetails::new(&mut conn, &name, &version));
302301

303302
CrateDetailsPage { details }.into_response(req)
304303
}
305304

306-
Some(MatchSemver::Semver((version, _))) => {
305+
Ok(MatchSemver::Semver((version, _))) => {
307306
let url = ctry!(
308307
req,
309308
Url::parse(&format!(
@@ -317,7 +316,7 @@ pub fn crate_details_handler(req: &mut Request) -> IronResult<Response> {
317316
Ok(super::redirect(url))
318317
}
319318

320-
None => Err(IronError::new(Nope::CrateNotFound, status::NotFound)),
319+
Err(err) => Err(IronError::new(err, status::NotFound)),
321320
}
322321
}
323322

src/web/error.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use std::{error::Error, fmt};
1010
pub enum Nope {
1111
ResourceNotFound,
1212
CrateNotFound,
13+
VersionNotFound,
1314
NoResults,
1415
InternalServerError,
1516
}
@@ -19,6 +20,7 @@ impl fmt::Display for Nope {
1920
f.write_str(match *self {
2021
Nope::ResourceNotFound => "Requested resource not found",
2122
Nope::CrateNotFound => "Requested crate not found",
23+
Nope::VersionNotFound => "Requested crate does not have specified version",
2224
Nope::NoResults => "Search yielded no results",
2325
Nope::InternalServerError => "Internal server error",
2426
})
@@ -52,6 +54,17 @@ impl Handler for Nope {
5254
.into_response(req)
5355
}
5456

57+
Nope::VersionNotFound => {
58+
// user tried to navigate to a crate with a version that does not exist
59+
// TODO: Display the attempted crate and version
60+
ErrorPage {
61+
title: "The requested version does not exist",
62+
message: Some("no such version for this crate".into()),
63+
status: Status::NotFound,
64+
}
65+
.into_response(req)
66+
}
67+
5568
Nope::NoResults => {
5669
let mut params = req.url.as_ref().query_pairs();
5770

src/web/mod.rs

Lines changed: 42 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ mod statics;
9191

9292
use crate::{impl_webpage, Context};
9393
use chrono::{DateTime, Utc};
94+
use error::Nope;
9495
use extensions::InjectExtensions;
9596
use failure::Error;
9697
use iron::{
@@ -177,13 +178,14 @@ impl Handler for CratesfyiHandler {
177178
}
178179
};
179180

180-
// try serving shared rustdoc resources first, then router, then db/static file handler
181-
// return 404 if none of them return Ok
181+
// try serving shared rustdoc resources first, then db/static file handler and last router
182+
// return 404 if none of them return Ok. It is important that the router comes last,
183+
// because it gives the most specific errors, e.g. CrateNotFound or VersionNotFound
182184
self.shared_resource_handler
183185
.handle(req)
184-
.or_else(|e| if_404(e, || self.router_handler.handle(req)))
185186
.or_else(|e| if_404(e, || self.database_file_handler.handle(req)))
186187
.or_else(|e| if_404(e, || self.static_handler.handle(req)))
188+
.or_else(|e| if_404(e, || self.router_handler.handle(req)))
187189
.or_else(|e| {
188190
let err = if let Some(err) = e.error.downcast::<error::Nope>() {
189191
*err
@@ -245,12 +247,12 @@ struct MatchVersion {
245247
impl MatchVersion {
246248
/// If the matched version was an exact match to the requested crate name, returns the
247249
/// `MatchSemver` for the query. If the lookup required a dash/underscore conversion, returns
248-
/// `None`.
249-
fn assume_exact(self) -> Option<MatchSemver> {
250+
/// `Nope::CrateNotFound`.
251+
fn assume_exact(self) -> Result<MatchSemver, Nope> {
250252
if self.corrected_name.is_none() {
251-
Some(self.version)
253+
Ok(self.version)
252254
} else {
253-
None
255+
Err(Nope::CrateNotFound)
254256
}
255257
}
256258
}
@@ -284,7 +286,11 @@ impl MatchSemver {
284286
/// This function will also check for crates where dashes in the name (`-`) have been replaced with
285287
/// underscores (`_`) and vice-versa. The return value will indicate whether the crate name has
286288
/// been matched exactly, or if there has been a "correction" in the name that matched instead.
287-
fn match_version(conn: &mut Client, name: &str, version: Option<&str>) -> Option<MatchVersion> {
289+
fn match_version(
290+
conn: &mut Client,
291+
name: &str,
292+
version: Option<&str>,
293+
) -> Result<MatchVersion, Nope> {
288294
// version is an Option<&str> from router::Router::get, need to decode first
289295
use iron::url::percent_encoding::percent_decode;
290296

@@ -320,24 +326,31 @@ fn match_version(conn: &mut Client, name: &str, version: Option<&str>) -> Option
320326
.collect()
321327
};
322328

329+
if versions.is_empty() {
330+
return Err(Nope::CrateNotFound);
331+
}
332+
323333
// first check for exact match, we can't expect users to use semver in query
324334
if let Some((version, id, _)) = versions.iter().find(|(vers, _, _)| vers == &req_version) {
325-
return Some(MatchVersion {
335+
return Ok(MatchVersion {
326336
corrected_name,
327337
version: MatchSemver::Exact((version.to_owned(), *id)),
328338
});
329339
}
330340

331341
// Now try to match with semver
332-
let req_sem_ver = VersionReq::parse(&req_version).ok()?;
342+
let req_sem_ver = VersionReq::parse(&req_version).map_err(|_| Nope::CrateNotFound)?;
333343

334344
// we need to sort versions first
335345
let versions_sem = {
336346
let mut versions_sem: Vec<(Version, i32)> = Vec::with_capacity(versions.len());
337347

338348
for version in versions.iter().filter(|(_, _, yanked)| !yanked) {
339349
// in theory a crate must always have a semver compatible version, but check result just in case
340-
versions_sem.push((Version::parse(&version.0).ok()?, version.1));
350+
versions_sem.push((
351+
Version::parse(&version.0).map_err(|_| Nope::InternalServerError)?,
352+
version.1,
353+
));
341354
}
342355

343356
versions_sem.sort();
@@ -349,7 +362,7 @@ fn match_version(conn: &mut Client, name: &str, version: Option<&str>) -> Option
349362
.iter()
350363
.find(|(vers, _)| req_sem_ver.matches(vers))
351364
{
352-
return Some(MatchVersion {
365+
return Ok(MatchVersion {
353366
corrected_name,
354367
version: MatchSemver::Semver((version.to_string(), *id)),
355368
});
@@ -358,13 +371,18 @@ fn match_version(conn: &mut Client, name: &str, version: Option<&str>) -> Option
358371
// semver is acting weird for '*' (any) range if a crate only have pre-release versions
359372
// return first non-yanked version if requested version is '*'
360373
if req_version == "*" {
361-
return versions_sem.first().map(|v| MatchVersion {
362-
corrected_name,
363-
version: MatchSemver::Semver((v.0.to_string(), v.1)),
364-
});
374+
return versions_sem
375+
.first()
376+
.map(|v| MatchVersion {
377+
corrected_name,
378+
version: MatchSemver::Semver((v.0.to_string(), v.1)),
379+
})
380+
.ok_or(Nope::CrateNotFound);
365381
}
366382

367-
None
383+
// Since we return with a CrateNotFound earlier if the db reply is empty,
384+
// we know that versions were returned but none satisfied the version requirement
385+
Err(Nope::VersionNotFound)
368386
}
369387

370388
/// Wrapper around the Markdown parser and renderer to render markdown
@@ -581,7 +599,13 @@ mod test {
581599

582600
fn version(v: Option<&str>, db: &TestDatabase) -> Option<String> {
583601
match_version(&mut db.conn(), "foo", v)
584-
.and_then(|version| version.assume_exact().map(|semver| semver.into_parts().0))
602+
.ok()
603+
.and_then(|version| {
604+
version
605+
.assume_exact()
606+
.ok()
607+
.map(|semver| semver.into_parts().0)
608+
})
585609
}
586610

587611
fn semver(version: &'static str) -> Option<String> {

src/web/releases.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -557,7 +557,7 @@ pub fn search_handler(req: &mut Request) -> IronResult<Response> {
557557
// since we never pass a version into `match_version` here, we'll never get
558558
// `MatchVersion::Exact`, so the distinction between `Exact` and `Semver` doesn't
559559
// matter
560-
if let Some(matchver) = match_version(&mut conn, &query, None) {
560+
if let Ok(matchver) = match_version(&mut conn, &query, None) {
561561
let (version, id) = matchver.version.into_parts();
562562
let query = matchver.corrected_name.unwrap_or_else(|| query.to_string());
563563

src/web/rustdoc.rs

Lines changed: 25 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -143,16 +143,16 @@ pub fn rustdoc_redirector_handler(req: &mut Request) -> IronResult<Response> {
143143
// it doesn't matter if the version that was given was exact or not, since we're redirecting
144144
// anyway
145145
let (version, id) = match match_version(&mut conn, &crate_name, req_version) {
146-
Some(v) => {
146+
Ok(v) => {
147147
if let Some(new_name) = v.corrected_name {
148148
// `match_version` checked against -/_ typos, so if we have a name here we should
149149
// use that instead
150150
crate_name = new_name;
151151
}
152152
v.version.into_parts()
153153
}
154-
None => {
155-
return Err(IronError::new(Nope::CrateNotFound, status::NotFound));
154+
Err(err) => {
155+
return Err(IronError::new(err, status::NotFound));
156156
}
157157
};
158158

@@ -286,28 +286,29 @@ pub fn rustdoc_html_server_handler(req: &mut Request) -> IronResult<Response> {
286286
// * If there is an exact match, but the requested crate name was corrected (dashes vs. underscores), redirect to the corrected name.
287287
// * If there is a semver (but not exact) match, redirect to the exact version.
288288
// * Otherwise, return a 404.
289-
let version = if let Some(match_vers) = match_version(&mut conn, &name, url_version) {
290-
match match_vers.version {
291-
MatchSemver::Exact((version, _)) => {
292-
// Redirect when the requested crate name isn't correct
293-
if let Some(name) = match_vers.corrected_name {
294-
return redirect(&name, &version, &req_path);
295-
}
289+
let version = match match_version(&mut conn, &name, url_version) {
290+
Ok(match_vers) => {
291+
match match_vers.version {
292+
MatchSemver::Exact((version, _)) => {
293+
// Redirect when the requested crate name isn't correct
294+
if let Some(name) = match_vers.corrected_name {
295+
return redirect(&name, &version, &req_path);
296+
}
296297

297-
version
298-
}
298+
version
299+
}
299300

300-
// Redirect when the requested version isn't correct
301-
MatchSemver::Semver((v, _)) => {
302-
// to prevent cloudfront caching the wrong artifacts on URLs with loose semver
303-
// versions, redirect the browser to the returned version instead of loading it
304-
// immediately
305-
return redirect(&name, &v, &req_path);
301+
// Redirect when the requested version isn't correct
302+
MatchSemver::Semver((v, _)) => {
303+
// to prevent cloudfront caching the wrong artifacts on URLs with loose semver
304+
// versions, redirect the browser to the returned version instead of loading it
305+
// immediately
306+
return redirect(&name, &v, &req_path);
307+
}
306308
}
307309
}
308-
} else {
309-
// Return a 404, as a crate by that name and version doesn't exist
310-
return Err(IronError::new(Nope::ResourceNotFound, status::NotFound));
310+
// Return a 404, as a crate by that name and version requirement doesn't exist
311+
Err(err) => return Err(IronError::new(err, status::NotFound)),
311312
};
312313

313314
rendering_time.step("crate details");
@@ -527,7 +528,7 @@ pub fn badge_handler(req: &mut Request) -> IronResult<Response> {
527528

528529
let options =
529530
match match_version(&mut conn, &name, Some(&version)).and_then(|m| m.assume_exact()) {
530-
Some(MatchSemver::Exact((version, id))) => {
531+
Ok(MatchSemver::Exact((version, id))) => {
531532
let rows = ctry!(
532533
req,
533534
conn.query(
@@ -552,7 +553,7 @@ pub fn badge_handler(req: &mut Request) -> IronResult<Response> {
552553
}
553554
}
554555

555-
Some(MatchSemver::Semver((version, _))) => {
556+
Ok(MatchSemver::Semver((version, _))) => {
556557
let base_url = format!("{}/{}/badge.svg", redirect_base(req), name);
557558
let url = ctry!(
558559
req,
@@ -562,7 +563,7 @@ pub fn badge_handler(req: &mut Request) -> IronResult<Response> {
562563
return Ok(super::redirect(iron_url));
563564
}
564565

565-
None => BadgeOptions {
566+
Err(_) => BadgeOptions {
566567
subject: "docs".to_owned(),
567568
status: "no builds".to_owned(),
568569
color: "#e05d44".to_owned(),

0 commit comments

Comments
 (0)