use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use systemprompt_config::SecretsBootstrapError;
use systemprompt_loader::ConfigLoadError;
use systemprompt_models::api::ApiError;
use super::super::bridge_error::BridgeError;
use crate::error::ApiHttpError;
#[derive(Debug, thiserror::Error)]
pub enum ReleaseError {
#[error(transparent)]
Auth(Box<BridgeError>),
#[error("services config not ready")]
ServicesNotReady(#[source] ConfigLoadError),
#[error("bridge releases are not configured on this gateway")]
NotConfigured,
#[error("no published build for platform {platform}")]
UnknownPlatform { platform: String },
#[error("no {prefix}* release found in {repo}")]
NoRelease { prefix: String, repo: String },
#[error("release {tag} has no asset {asset}")]
MissingAsset { tag: String, asset: String },
#[error("release {version} publishes no SHA256SUMS")]
NoChecksums { version: String },
#[error("SHA256SUMS has no entry for {asset}")]
ChecksumMissing { asset: String },
#[error("{stage} failed")]
Request {
stage: &'static str,
#[source]
source: reqwest::Error,
},
#[error("{stage} returned {status}")]
UpstreamStatus {
stage: &'static str,
status: StatusCode,
},
#[error(
"{stage} returned {status}: GitHub rejected the release token from secret {key}; rotate \
that secret (bridge_releases.token_secret)"
)]
TokenRejected {
stage: &'static str,
status: StatusCode,
key: String,
},
#[error("bridge release token secret unavailable")]
SecretsUnavailable(#[source] SecretsBootstrapError),
#[error("bridge release token secret {key} is not configured")]
TokenNotConfigured { key: String },
}
impl ReleaseError {
#[must_use]
pub fn upstream(
stage: &'static str,
resp: &reqwest::Response,
token_secret: Option<&str>,
) -> Self {
let status = resp.status();
let rate_limited = resp
.headers()
.get("x-ratelimit-remaining")
.is_some_and(|remaining| remaining == "0");
let refused = status == StatusCode::UNAUTHORIZED
|| (status == StatusCode::FORBIDDEN && !rate_limited);
match token_secret {
Some(key) if refused => Self::TokenRejected {
stage,
status,
key: key.to_owned(),
},
_ => Self::UpstreamStatus { stage, status },
}
}
#[must_use]
pub const fn status(&self) -> StatusCode {
match self {
Self::Auth(_) => StatusCode::UNAUTHORIZED,
Self::NotConfigured
| Self::UnknownPlatform { .. }
| Self::NoRelease { .. }
| Self::MissingAsset { .. } => StatusCode::NOT_FOUND,
Self::NoChecksums { .. }
| Self::ChecksumMissing { .. }
| Self::Request { .. }
| Self::UpstreamStatus { .. } => StatusCode::BAD_GATEWAY,
Self::ServicesNotReady(_)
| Self::SecretsUnavailable(_)
| Self::TokenNotConfigured { .. }
| Self::TokenRejected { .. } => StatusCode::SERVICE_UNAVAILABLE,
}
}
#[must_use]
pub const fn stage(&self) -> Option<&'static str> {
match self {
Self::Request { stage, .. }
| Self::UpstreamStatus { stage, .. }
| Self::TokenRejected { stage, .. } => Some(*stage),
_ => None,
}
}
#[must_use]
pub fn upstream_status(&self) -> Option<StatusCode> {
match self {
Self::UpstreamStatus { status, .. } | Self::TokenRejected { status, .. } => {
Some(*status)
},
Self::Request { source, .. } => source.status(),
_ => None,
}
}
#[must_use]
pub const fn error_key(&self) -> &'static str {
match self {
Self::Auth(_) => "invalid_credential",
Self::NotConfigured => "bridge_releases_not_configured",
Self::UnknownPlatform { .. } => "platform_not_published",
Self::NoRelease { .. } => "release_not_found",
Self::MissingAsset { .. } => "release_asset_not_found",
Self::NoChecksums { .. } | Self::ChecksumMissing { .. } => {
"release_checksum_unavailable"
},
Self::Request { .. } | Self::UpstreamStatus { .. } => "release_upstream_failed",
Self::TokenRejected { .. } => "release_token_rejected",
Self::ServicesNotReady(_)
| Self::SecretsUnavailable(_)
| Self::TokenNotConfigured { .. } => "release_feed_not_ready",
}
}
}
impl From<BridgeError> for ReleaseError {
fn from(err: BridgeError) -> Self {
Self::Auth(Box::new(err))
}
}
impl IntoResponse for ReleaseError {
fn into_response(self) -> Response {
let status = self.status();
let key = self.error_key();
let api = match self {
Self::Auth(inner) => return ApiHttpError::from(*inner).into_response(),
err if status == StatusCode::NOT_FOUND => {
ApiError::not_found(err.to_string()).with_error_key(key)
},
err => {
tracing::error!(
status = status.as_u16(),
error_key = key,
stage = err.stage().unwrap_or("-"),
upstream_status = err.upstream_status().map(|s| s.as_u16()),
error = %err,
"bridge release feed failed"
);
ApiError::service_unavailable("Bridge release feed unavailable")
.with_error_key(key)
.with_source(err)
},
};
let mut response = api.into_response();
*response.status_mut() = status;
response
}
}