Skip to content

Commit ffe8a03

Browse files
committed
gracefully handle 404 errors from crates.io
1 parent 9f1ff9e commit ffe8a03

6 files changed

Lines changed: 119 additions & 33 deletions

File tree

Cargo.lock

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/bin/docs_rs_builder/src/docbuilder/rustwide_builder.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -786,7 +786,7 @@ impl RustwideBuilder {
786786
.runtime
787787
.block_on(self.registry_api.get_release_data(name, version))
788788
{
789-
Ok(data) => Some(data),
789+
Ok(data) => data,
790790
Err(err) => {
791791
error!(%name, %version, ?err, "could not fetch releases-data");
792792
None

crates/bin/docs_rs_import_release/src/import.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use crate::{
44
rustdoc::{download_static_files, find_static_paths, find_successful_build_targets},
55
rustdoc_status::fetch_rustdoc_status,
66
};
7+
use anyhow::anyhow;
78
use anyhow::{Result, bail};
89
use docs_rs_cargo_metadata::CargoMetadata;
910
use docs_rs_database::releases::{
@@ -117,7 +118,10 @@ async fn import_test_release_inner(
117118
(files_list, source_size)
118119
};
119120

120-
let registry_data = registry_api.get_release_data(name, version).await?;
121+
let registry_data = registry_api
122+
.get_release_data(name, version)
123+
.await?
124+
.ok_or_else(|| anyhow!("registry data not found"))?;
121125

122126
let rustdoc_dir = {
123127
info!("download & extract rustdoc archive...");

crates/lib/docs_rs_registry_api/Cargo.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,10 @@ serde = { workspace = true }
1818
sqlx = { workspace = true }
1919
tracing = { workspace = true }
2020
url = { workspace = true }
21+
22+
[dev-dependencies]
23+
docs_rs_types = { path = "../docs_rs_types", features = ["testing"] }
24+
mime = { workspace = true }
25+
mockito = { workspace = true }
26+
serde_json = { workspace = true }
27+
tokio = { workspace = true }

crates/lib/docs_rs_registry_api/src/api.rs

Lines changed: 101 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ use anyhow::{Context, Result, anyhow, bail};
66
use chrono::{DateTime, Utc};
77
use docs_rs_types::{KrateName, Version};
88
use docs_rs_utils::{APP_USER_AGENT, retry_async};
9-
use reqwest::header::{ACCEPT, HeaderValue, USER_AGENT};
9+
use reqwest::{
10+
StatusCode,
11+
header::{ACCEPT, HeaderValue, USER_AGENT},
12+
};
1013
use serde::Deserialize;
1114
use tracing::instrument;
1215
use url::Url;
@@ -60,25 +63,7 @@ impl RegistryApi {
6063
&self,
6164
name: &KrateName,
6265
version: &Version,
63-
) -> Result<ReleaseData> {
64-
let (release_time, yanked, downloads) = self
65-
.get_release_time_yanked_downloads(name, version)
66-
.await
67-
.context(format!("Failed to get crate data for {name}-{version}"))?;
68-
69-
Ok(ReleaseData {
70-
release_time,
71-
yanked,
72-
downloads,
73-
})
74-
}
75-
76-
/// Get release_time, yanked and downloads from the registry's API
77-
async fn get_release_time_yanked_downloads(
78-
&self,
79-
name: &KrateName,
80-
version: &Version,
81-
) -> Result<(DateTime<Utc>, bool, i32)> {
66+
) -> Result<Option<ReleaseData>> {
8267
let url = {
8368
let mut url = self.api_base.clone();
8469
url.path_segments_mut()
@@ -103,28 +88,43 @@ impl RegistryApi {
10388
downloads: i32,
10489
}
10590

106-
let response: Response = retry_async(
91+
let response: Response = match retry_async(
10792
|| async {
108-
Ok(self
109-
.client
110-
.get(url.clone())
111-
.send()
112-
.await?
113-
.error_for_status()?)
93+
Ok(
94+
match self
95+
.client
96+
.get(url.clone())
97+
.send()
98+
.await?
99+
.error_for_status()
100+
{
101+
Ok(resp) => Some(resp),
102+
Err(err) if matches!(err.status(), Some(StatusCode::NOT_FOUND)) => None,
103+
Err(err) => return Err(err.into()),
104+
},
105+
)
114106
},
115107
self.max_retries,
116108
)
117109
.await?
118-
.json()
119-
.await?;
110+
{
111+
Some(resp) => resp.json().await?,
112+
None => {
113+
return Ok(None);
114+
}
115+
};
120116

121117
let version = response
122118
.versions
123119
.into_iter()
124120
.find(|data| data.num == *version)
125121
.with_context(|| anyhow!("Could not find version in response"))?;
126122

127-
Ok((version.created_at, version.yanked, version.downloads))
123+
Ok(Some(ReleaseData {
124+
release_time: version.created_at,
125+
yanked: version.yanked,
126+
downloads: version.downloads,
127+
}))
128128
}
129129

130130
/// Fetch owners from the registry's API
@@ -241,3 +241,74 @@ impl RegistryApi {
241241
Ok(Search { crates, meta })
242242
}
243243
}
244+
245+
#[cfg(test)]
246+
mod tests {
247+
use super::*;
248+
use anyhow::Result;
249+
use docs_rs_types::testing::{KRATE, V1, V2};
250+
use reqwest::header::CONTENT_TYPE;
251+
252+
#[tokio::test]
253+
async fn test_get_release_data() -> Result<()> {
254+
let mut server = mockito::Server::new_async().await;
255+
256+
let created = Utc::now();
257+
258+
let _m = server
259+
.mock("GET", "/api/v1/crates/krate/versions")
260+
.with_status(200)
261+
.with_header(CONTENT_TYPE, mime::APPLICATION_JSON.as_ref())
262+
.with_body(
263+
serde_json::json!({
264+
"versions": [
265+
{
266+
"num": V1.to_string(),
267+
"created_at": created.to_rfc3339(),
268+
"yanked": false,
269+
"downloads": 42
270+
},
271+
{
272+
"num": V2.to_string(),
273+
"created_at": "2025-01-01T00:00:00Z",
274+
"yanked": true,
275+
"downloads": 22
276+
}
277+
]
278+
})
279+
.to_string(),
280+
)
281+
.create_async()
282+
.await;
283+
284+
let api = RegistryApi::new(server.url().parse().unwrap(), 0)?;
285+
286+
assert_eq!(
287+
api.get_release_data(&KRATE, &V1).await?,
288+
Some(ReleaseData {
289+
release_time: created,
290+
yanked: false,
291+
downloads: 42
292+
})
293+
);
294+
295+
Ok(())
296+
}
297+
298+
#[tokio::test]
299+
async fn test_404_in_release_data_returns_none() -> Result<()> {
300+
let mut server = mockito::Server::new_async().await;
301+
302+
let _m = server
303+
.mock("GET", "/api/v1/crates/krate/versions")
304+
.with_status(404)
305+
.create_async()
306+
.await;
307+
308+
let api = RegistryApi::new(server.url().parse().unwrap(), 0)?;
309+
310+
assert_eq!(api.get_release_data(&KRATE, &V1).await?, None,);
311+
312+
Ok(())
313+
}
314+
}

crates/lib/docs_rs_registry_api/src/models.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ pub struct CrateData {
77
pub owners: Vec<CrateOwner>,
88
}
99

10-
#[derive(Debug)]
10+
#[derive(Debug, PartialEq)]
1111
pub struct ReleaseData {
1212
pub release_time: DateTime<Utc>,
1313
pub yanked: bool,

0 commit comments

Comments
 (0)