From 2ba1b48a8c965e356c224919f4f855f71133befe Mon Sep 17 00:00:00 2001 From: Denis Cornehl Date: Wed, 11 Feb 2026 00:18:43 +0100 Subject: [PATCH] registry-api: gracefully handle releases not found --- .../src/docbuilder/rustwide_builder.rs | 2 +- .../bin/docs_rs_import_release/src/import.rs | 7 +- crates/lib/docs_rs_registry_api/Cargo.toml | 1 + crates/lib/docs_rs_registry_api/src/api.rs | 136 ++++++++++++++++-- crates/lib/docs_rs_registry_api/src/error.rs | 1 - crates/lib/docs_rs_registry_api/src/models.rs | 1 + 6 files changed, 136 insertions(+), 12 deletions(-) diff --git a/crates/bin/docs_rs_builder/src/docbuilder/rustwide_builder.rs b/crates/bin/docs_rs_builder/src/docbuilder/rustwide_builder.rs index 0de95fbfe..75e49cb09 100644 --- a/crates/bin/docs_rs_builder/src/docbuilder/rustwide_builder.rs +++ b/crates/bin/docs_rs_builder/src/docbuilder/rustwide_builder.rs @@ -786,7 +786,7 @@ impl RustwideBuilder { .runtime .block_on(self.registry_api.get_release_data(name, version)) { - Ok(data) => Some(data), + Ok(data) => data, Err(err) => { error!(%name, %version, ?err, "could not fetch releases-data"); None diff --git a/crates/bin/docs_rs_import_release/src/import.rs b/crates/bin/docs_rs_import_release/src/import.rs index dbf90a921..a11fa0c30 100644 --- a/crates/bin/docs_rs_import_release/src/import.rs +++ b/crates/bin/docs_rs_import_release/src/import.rs @@ -4,7 +4,7 @@ use crate::{ rustdoc::{download_static_files, find_static_paths, find_successful_build_targets}, rustdoc_status::fetch_rustdoc_status, }; -use anyhow::{Result, bail}; +use anyhow::{Result, anyhow, bail}; use docs_rs_cargo_metadata::CargoMetadata; use docs_rs_database::releases::{ finish_build, finish_release, initialize_build, initialize_crate, initialize_release, @@ -117,7 +117,10 @@ async fn import_test_release_inner( (files_list, source_size) }; - let registry_data = registry_api.get_release_data(name, version).await?; + let registry_data = registry_api + .get_release_data(name, version) + .await? + .ok_or_else(|| anyhow!("release not found in registry"))?; let rustdoc_dir = { info!("download & extract rustdoc archive..."); diff --git a/crates/lib/docs_rs_registry_api/Cargo.toml b/crates/lib/docs_rs_registry_api/Cargo.toml index fc5f8faa2..deb83cddc 100644 --- a/crates/lib/docs_rs_registry_api/Cargo.toml +++ b/crates/lib/docs_rs_registry_api/Cargo.toml @@ -22,6 +22,7 @@ tracing = { workspace = true } url = { workspace = true } [dev-dependencies] +docs_rs_types = { path = "../docs_rs_types", features = ["testing"] } mime = { workspace = true } mockito = { workspace = true } test-case = { workspace = true } diff --git a/crates/lib/docs_rs_registry_api/src/api.rs b/crates/lib/docs_rs_registry_api/src/api.rs index 492a914d7..6278248cd 100644 --- a/crates/lib/docs_rs_registry_api/src/api.rs +++ b/crates/lib/docs_rs_registry_api/src/api.rs @@ -3,11 +3,13 @@ use crate::{ error::{Error, Result}, models::{ApiErrors, CrateData, CrateOwner, OwnerKind, ReleaseData, Search, SearchResponse}, }; -use anyhow::{Context, anyhow}; use chrono::{DateTime, Utc}; use docs_rs_types::{KrateName, Version}; use docs_rs_utils::{APP_USER_AGENT, retry_async}; -use reqwest::header::{ACCEPT, HeaderValue, USER_AGENT}; +use reqwest::{ + StatusCode, + header::{ACCEPT, HeaderValue, USER_AGENT}, +}; use serde::{Deserialize, de::DeserializeOwned}; use tracing::instrument; use url::Url; @@ -111,7 +113,7 @@ impl RegistryApi { &self, name: &KrateName, version: &Version, - ) -> Result { + ) -> Result> { let url = { let mut url = self.api_base.clone(); url.path_segments_mut() @@ -136,19 +138,25 @@ impl RegistryApi { downloads: i32, } - let response: Response = self.request(&url).await?; + let response: Response = match self.request(&url).await { + Ok(response) => response, + Err(err) if err.status() == Some(StatusCode::NOT_FOUND) => return Ok(None), + Err(err) => return Err(err), + }; - let version = response + let Some(version) = response .versions .into_iter() .find(|data| data.num == *version) - .with_context(|| anyhow!("Could not find version in response"))?; + else { + return Ok(None); + }; - Ok(ReleaseData { + Ok(Some(ReleaseData { release_time: version.created_at, yanked: version.yanked, downloads: version.downloads, - }) + })) } /// Fetch owners from the registry's API @@ -222,6 +230,8 @@ impl RegistryApi { mod tests { use super::*; use crate::models::{ApiError, SearchCrate, SearchMeta}; + use anyhow::anyhow; + use docs_rs_types::testing::{KRATE, V1, V2}; use reqwest::{StatusCode, header::CONTENT_TYPE}; use serde::Serialize; use test_case::test_case; @@ -241,6 +251,25 @@ mod tests { api.search("q=foo").await } + async fn test_get_release( + status: StatusCode, + body: impl Serialize, + version: &Version, + ) -> Result> { + let mut crates_io_api = mockito::Server::new_async().await; + + let _m = crates_io_api + .mock("GET", "/api/v1/crates/krate/versions") + .with_status(status.as_u16().into()) + .with_header(CONTENT_TYPE, mime::APPLICATION_JSON.as_ref()) + .with_body(serde_json::to_vec(&body).unwrap()) + .create_async() + .await; + + let api = RegistryApi::new(crates_io_api.url().parse().unwrap(), 0)?; + api.get_release_data(&KRATE, version).await + } + #[test] fn test_error_without_status() { for err in [ @@ -397,4 +426,95 @@ mod tests { Ok(()) } + + #[tokio::test] + async fn test_get_release_ok() -> Result<()> { + let release_data = test_get_release( + StatusCode::OK, + serde_json::json!({ + "versions": [ + { + "num": V1.to_string(), + "created_at": "2024-01-01T00:00:00Z", + "yanked": false, + "downloads": 42 + }, + { + "num": V2.to_string(), + "created_at": "2024-01-02T00:00:00Z", + "yanked": true, + "downloads": 100 + } + ] + }), + &V1, + ) + .await? + .expect("found version"); + + assert_eq!( + release_data, + ReleaseData { + release_time: DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z") + .unwrap() + .with_timezone(&Utc), + yanked: false, + downloads: 42, + } + ); + + Ok(()) + } + + #[tokio::test] + async fn test_get_release_not_found_empty_result() -> Result<()> { + assert!( + test_get_release( + StatusCode::OK, + serde_json::json!({ + "versions": [] + }), + &V1, + ) + .await? + .is_none() + ); + + Ok(()) + } + + #[tokio::test] + async fn test_get_release_not_found_other_version() -> Result<()> { + assert!( + test_get_release( + StatusCode::OK, + serde_json::json!({ + "versions": [ + { + "num": V1.to_string(), + "created_at": "2024-01-01T00:00:00Z", + "yanked": false, + "downloads": 42 + } + ] + }), + &V2, + ) + .await? + .is_none() + ); + + Ok(()) + } + + #[tokio::test] + async fn test_get_release_not_found_404() -> Result<()> { + assert!( + test_get_release(StatusCode::NOT_FOUND, "", &V1) + .await? + .is_none() + ); + + Ok(()) + } } diff --git a/crates/lib/docs_rs_registry_api/src/error.rs b/crates/lib/docs_rs_registry_api/src/error.rs index 50cdd4607..e46bb32d6 100644 --- a/crates/lib/docs_rs_registry_api/src/error.rs +++ b/crates/lib/docs_rs_registry_api/src/error.rs @@ -61,7 +61,6 @@ mod tests { let status = StatusCode::INTERNAL_SERVER_ERROR; assert!(Error::CrateIoApiError(status, ApiErrors::default()).status() == Some(status)); - assert!(Error::CrateIoError(status, "".into()).status() == Some(status)); } diff --git a/crates/lib/docs_rs_registry_api/src/models.rs b/crates/lib/docs_rs_registry_api/src/models.rs index 2c5ee5cf4..ac797dcbe 100644 --- a/crates/lib/docs_rs_registry_api/src/models.rs +++ b/crates/lib/docs_rs_registry_api/src/models.rs @@ -8,6 +8,7 @@ pub struct CrateData { } #[derive(Debug)] +#[cfg_attr(test, derive(PartialEq))] pub struct ReleaseData { pub release_time: DateTime, pub yanked: bool,