Skip to main content

systemprompt_api/routes/gateway/bridge_release/
error.rs

1//! Typed failures of the bridge release feed and their HTTP answer.
2//!
3//! Every bridge in the fleet reads this feed on a timer, so a resolution that
4//! keeps failing is a fleet-wide update outage: an upstream failure answers
5//! 502/503 through `ApiError`, which logs it at error with its full cause
6//! chain while the body carries only the fixed public text. A configuration
7//! answer (no build for the platform, no release, no asset) stays a 404.
8//!
9//! Copyright (c) systemprompt.io — Business Source License 1.1.
10//! See <https://systemprompt.io> for licensing details.
11
12use axum::http::StatusCode;
13use axum::response::{IntoResponse, Response};
14use systemprompt_config::SecretsBootstrapError;
15use systemprompt_loader::ConfigLoadError;
16use systemprompt_models::api::ApiError;
17
18use super::super::bridge_error::BridgeError;
19use crate::error::ApiHttpError;
20
21#[derive(Debug, thiserror::Error)]
22pub enum ReleaseError {
23    #[error(transparent)]
24    Auth(Box<BridgeError>),
25    #[error("services config not ready")]
26    ServicesNotReady(#[source] ConfigLoadError),
27    #[error("bridge releases are not configured on this gateway")]
28    NotConfigured,
29    #[error("no published build for platform {platform}")]
30    UnknownPlatform { platform: String },
31    #[error("no {prefix}* release found in {repo}")]
32    NoRelease { prefix: String, repo: String },
33    #[error("release {tag} has no asset {asset}")]
34    MissingAsset { tag: String, asset: String },
35    #[error("release {version} publishes no SHA256SUMS")]
36    NoChecksums { version: String },
37    #[error("SHA256SUMS has no entry for {asset}")]
38    ChecksumMissing { asset: String },
39    #[error("{stage} failed")]
40    Request {
41        stage: &'static str,
42        #[source]
43        source: reqwest::Error,
44    },
45    #[error("{stage} returned {status}")]
46    UpstreamStatus {
47        stage: &'static str,
48        status: StatusCode,
49    },
50    #[error("bridge release token secret unavailable")]
51    SecretsUnavailable(#[source] SecretsBootstrapError),
52    #[error("bridge release token secret {key} is not configured")]
53    TokenNotConfigured { key: String },
54}
55
56impl ReleaseError {
57    #[must_use]
58    pub const fn status(&self) -> StatusCode {
59        match self {
60            Self::Auth(_) => StatusCode::UNAUTHORIZED,
61            Self::NotConfigured
62            | Self::UnknownPlatform { .. }
63            | Self::NoRelease { .. }
64            | Self::MissingAsset { .. } => StatusCode::NOT_FOUND,
65            Self::NoChecksums { .. }
66            | Self::ChecksumMissing { .. }
67            | Self::Request { .. }
68            | Self::UpstreamStatus { .. } => StatusCode::BAD_GATEWAY,
69            Self::ServicesNotReady(_)
70            | Self::SecretsUnavailable(_)
71            | Self::TokenNotConfigured { .. } => StatusCode::SERVICE_UNAVAILABLE,
72        }
73    }
74
75    #[must_use]
76    pub const fn error_key(&self) -> &'static str {
77        match self {
78            Self::Auth(_) => "invalid_credential",
79            Self::NotConfigured => "bridge_releases_not_configured",
80            Self::UnknownPlatform { .. } => "platform_not_published",
81            Self::NoRelease { .. } => "release_not_found",
82            Self::MissingAsset { .. } => "release_asset_not_found",
83            Self::NoChecksums { .. } | Self::ChecksumMissing { .. } => {
84                "release_checksum_unavailable"
85            },
86            Self::Request { .. } | Self::UpstreamStatus { .. } => "release_upstream_failed",
87            Self::ServicesNotReady(_)
88            | Self::SecretsUnavailable(_)
89            | Self::TokenNotConfigured { .. } => "release_feed_not_ready",
90        }
91    }
92}
93
94impl From<BridgeError> for ReleaseError {
95    fn from(err: BridgeError) -> Self {
96        Self::Auth(Box::new(err))
97    }
98}
99
100impl IntoResponse for ReleaseError {
101    fn into_response(self) -> Response {
102        let status = self.status();
103        let key = self.error_key();
104        let api = match self {
105            Self::Auth(inner) => return ApiHttpError::from(*inner).into_response(),
106            err if status == StatusCode::NOT_FOUND => {
107                ApiError::not_found(err.to_string()).with_error_key(key)
108            },
109            err => ApiError::service_unavailable("Bridge release feed unavailable")
110                .with_error_key(key)
111                .with_source(err),
112        };
113        let mut response = api.into_response();
114        // Why: `ErrorCode` has no 502, yet an upstream GitHub failure must still
115        // read as a bad gateway to clients, proxies and the access log, so the
116        // typed status is restored after the envelope renders.
117        *response.status_mut() = status;
118        response
119    }
120}