core: add non-panicking testing-endpoint APIs

This commit is contained in:
andrussal 2025-12-18 15:04:25 +01:00
parent 8a6d7236ef
commit e7093df4e5
3 changed files with 76 additions and 20 deletions

View File

@ -21,6 +21,14 @@ use tracing::error;
pub const DA_GET_TESTING_ENDPOINT_ERROR: &str = "Failed to connect to testing endpoint. The binary was likely built without the 'testing' \
feature. Try: cargo build --workspace --all-features";
#[derive(Debug, thiserror::Error)]
pub enum ApiClientError {
#[error("{DA_GET_TESTING_ENDPOINT_ERROR}")]
TestingEndpointUnavailable,
#[error(transparent)]
Request(#[from] reqwest::Error),
}
/// Thin async client for node HTTP/testing endpoints.
#[derive(Clone)]
pub struct ApiClient {
@ -154,6 +162,26 @@ impl ApiClient {
}
/// POST JSON to the testing API and return the raw response.
pub async fn post_testing_json_response_checked<T>(
&self,
path: &str,
body: &T,
) -> Result<Response, ApiClientError>
where
T: Serialize + Sync + ?Sized,
{
let testing_url = self
.testing_url
.as_ref()
.ok_or(ApiClientError::TestingEndpointUnavailable)?;
self.client
.post(Self::join_url(testing_url, path))
.json(body)
.send()
.await
.map_err(ApiClientError::Request)
}
pub async fn post_testing_json_response<T>(
&self,
path: &str,
@ -162,27 +190,39 @@ impl ApiClient {
where
T: Serialize + Sync + ?Sized,
{
let testing_url = self
.testing_url
.as_ref()
.expect(DA_GET_TESTING_ENDPOINT_ERROR);
self.client
.post(Self::join_url(testing_url, path))
.json(body)
.send()
.await
match self.post_testing_json_response_checked(path, body).await {
Ok(resp) => Ok(resp),
Err(ApiClientError::Request(err)) => Err(err),
Err(ApiClientError::TestingEndpointUnavailable) => {
panic!("{DA_GET_TESTING_ENDPOINT_ERROR}")
}
}
}
/// GET from the testing API and return the raw response.
pub async fn get_testing_response(&self, path: &str) -> reqwest::Result<Response> {
pub async fn get_testing_response_checked(
&self,
path: &str,
) -> Result<Response, ApiClientError> {
let testing_url = self
.testing_url
.as_ref()
.expect(DA_GET_TESTING_ENDPOINT_ERROR);
.ok_or(ApiClientError::TestingEndpointUnavailable)?;
self.client
.get(Self::join_url(testing_url, path))
.send()
.await
.map_err(ApiClientError::Request)
}
pub async fn get_testing_response(&self, path: &str) -> reqwest::Result<Response> {
match self.get_testing_response_checked(path).await {
Ok(resp) => Ok(resp),
Err(ApiClientError::Request(err)) => Err(err),
Err(ApiClientError::TestingEndpointUnavailable) => {
panic!("{DA_GET_TESTING_ENDPOINT_ERROR}")
}
}
}
/// Block a peer via the DA testing API.
@ -258,6 +298,19 @@ impl ApiClient {
}
/// Query DA membership via testing API.
pub async fn da_get_membership_checked(
&self,
session_id: &SessionNumber,
) -> Result<MembershipResponse, ApiClientError> {
self.post_testing_json_response_checked(DA_GET_MEMBERSHIP, session_id)
.await?
.error_for_status()
.map_err(ApiClientError::Request)?
.json()
.await
.map_err(ApiClientError::Request)
}
pub async fn da_get_membership(
&self,
session_id: &SessionNumber,

View File

@ -5,7 +5,7 @@ pub mod validator;
use std::sync::LazyLock;
pub use api_client::ApiClient;
pub use api_client::{ApiClient, ApiClientError};
use tempfile::TempDir;
pub(crate) const LOGS_PREFIX: &str = "__logs";

View File

@ -4,7 +4,7 @@ use reqwest::{Client, Url};
use thiserror::Error;
use super::ReadinessCheck;
use crate::topology::deployment::Topology;
use crate::{nodes::ApiClientError, topology::deployment::Topology};
#[derive(Debug, Error)]
pub enum MembershipError {
@ -15,7 +15,9 @@ pub enum MembershipError {
message: String,
},
#[error(transparent)]
Request(#[from] reqwest::Error),
Http(#[from] reqwest::Error),
#[error(transparent)]
ApiClient(#[from] ApiClientError),
}
#[derive(Debug)]
@ -50,7 +52,7 @@ impl<'a> ReadinessCheck<'a> for MembershipReadiness<'a> {
async move {
let result = node
.api()
.da_get_membership(&self.session)
.da_get_membership_checked(&self.session)
.await
.map_err(MembershipError::from);
NodeMembershipStatus { label, result }
@ -72,7 +74,7 @@ impl<'a> ReadinessCheck<'a> for MembershipReadiness<'a> {
async move {
let result = node
.api()
.da_get_membership(&self.session)
.da_get_membership_checked(&self.session)
.await
.map_err(MembershipError::from);
NodeMembershipStatus { label, result }
@ -164,12 +166,12 @@ pub async fn try_fetch_membership(
.json(&session)
.send()
.await
.map_err(MembershipError::Request)?
.map_err(MembershipError::Http)?
.error_for_status()
.map_err(MembershipError::Request)?
.map_err(MembershipError::Http)?
.json()
.await
.map_err(MembershipError::Request)
.map_err(MembershipError::Http)
}
#[deprecated(note = "use try_fetch_membership to avoid panics and preserve error details")]
@ -181,7 +183,7 @@ pub async fn fetch_membership(
try_fetch_membership(client, base, session)
.await
.map_err(|err| match err {
MembershipError::Request(source) => source,
MembershipError::Http(source) => source,
MembershipError::JoinUrl {
base,
path,
@ -189,6 +191,7 @@ pub async fn fetch_membership(
} => {
panic!("failed to join url {base} with path {path}: {message}")
}
MembershipError::ApiClient(err) => panic!("{err}"),
})
}