Skip to main content

fedimint_gateway_server/
error.rs

1use std::fmt::Display;
2
3use axum::Json;
4use axum::body::Body;
5use axum::response::{IntoResponse, Response};
6use fedimint_core::config::{FederationId, FederationIdPrefix};
7use fedimint_core::crit;
8use fedimint_core::envs::is_env_var_set;
9use fedimint_core::fmt_utils::OptStacktrace;
10use fedimint_core::util::FmtCompactAnyhow;
11use fedimint_gw_client::pay::OutgoingPaymentError;
12use fedimint_lightning::LightningRpcError;
13use fedimint_logging::LOG_GATEWAY;
14use reqwest::StatusCode;
15use thiserror::Error;
16use tracing::debug;
17
18use crate::envs::FM_DEBUG_GATEWAY_ENV;
19
20/// Top level error enum for all errors that can occur in the Gateway.
21#[derive(Debug, thiserror::Error)]
22pub enum GatewayError {
23    #[error("Admin error: {0}")]
24    Admin(#[from] AdminGatewayError),
25    #[error("Public error: {0}")]
26    Public(#[from] PublicGatewayError),
27    #[error("{0}")]
28    Lnurl(#[from] LnurlError),
29}
30
31impl IntoResponse for GatewayError {
32    fn into_response(self) -> Response {
33        match self {
34            GatewayError::Admin(admin) => admin.into_response(),
35            GatewayError::Public(public) => public.into_response(),
36            GatewayError::Lnurl(lnurl) => lnurl.into_response(),
37        }
38    }
39}
40
41/// Errors that unauthenticated endpoints can encounter. For privacy reasons,
42/// the error messages are intended to be redacted before returning to the
43/// client.
44#[derive(Debug, Error)]
45pub enum PublicGatewayError {
46    #[error("Gateway invariant violation: {}", OptStacktrace(.0))]
47    Internal(anyhow::Error),
48    #[error("Lightning rpc error: {}", .0)]
49    Lightning(#[from] LightningRpcError),
50    #[error("LNv1 error: {:?}", .0)]
51    LNv1(#[from] LNv1Error),
52    #[error("LNv2 error: {:?}", .0)]
53    LNv2(#[from] LNv2Error),
54    #[error("{}", .0)]
55    FederationNotConnected(#[from] FederationNotConnected),
56    #[error("Failed to receive ecash: {failure_reason}")]
57    ReceiveEcashError { failure_reason: String },
58    #[error("Too many requests")]
59    RateLimited,
60    #[error("Unexpected Error: {}", OptStacktrace(.0))]
61    Unexpected(#[from] anyhow::Error),
62}
63
64impl IntoResponse for PublicGatewayError {
65    fn into_response(self) -> Response {
66        // For privacy reasons, we do not return too many details about the failure of
67        // the request back to the client to prevent malicious clients from
68        // deducing state about the gateway/lightning node.
69        //
70        // Rate limit rejections are the one error a flood of requests emits at
71        // volume, so they are logged at debug level to keep them from spamming
72        // the log.
73        if matches!(self, PublicGatewayError::RateLimited) {
74            debug!(target: LOG_GATEWAY, "{self}");
75        } else {
76            crit!(target: LOG_GATEWAY, "{self}");
77        }
78        let (error_message, status_code) = match &self {
79            PublicGatewayError::Internal(_) => (
80                "Gateway internal error".to_owned(),
81                StatusCode::INTERNAL_SERVER_ERROR,
82            ),
83            PublicGatewayError::FederationNotConnected(e) => {
84                (e.to_string(), StatusCode::BAD_REQUEST)
85            }
86            PublicGatewayError::ReceiveEcashError { .. } => (
87                "Failed to receive ecash".to_string(),
88                StatusCode::INTERNAL_SERVER_ERROR,
89            ),
90            PublicGatewayError::Lightning(_) => (
91                "Lightning Network operation failed".to_string(),
92                StatusCode::INTERNAL_SERVER_ERROR,
93            ),
94            PublicGatewayError::LNv1(_) => (
95                "LNv1 operation failed, please contact gateway operator".to_string(),
96                StatusCode::INTERNAL_SERVER_ERROR,
97            ),
98            PublicGatewayError::LNv2(_) => (
99                "LNv2 operation failed, please contact gateway operator".to_string(),
100                StatusCode::INTERNAL_SERVER_ERROR,
101            ),
102            PublicGatewayError::RateLimited => (
103                "Too many requests, please try again later".to_string(),
104                StatusCode::TOO_MANY_REQUESTS,
105            ),
106            PublicGatewayError::Unexpected(e) => (e.to_string(), StatusCode::BAD_REQUEST),
107        };
108
109        let error_message =
110            self.response_message(error_message, is_env_var_set(FM_DEBUG_GATEWAY_ENV));
111
112        Response::builder()
113            .status(status_code)
114            .body(error_message.into())
115            .expect("Failed to create Response")
116    }
117}
118
119impl PublicGatewayError {
120    fn response_message(&self, sanitized: String, expose_debug_details: bool) -> String {
121        if expose_debug_details && !matches!(self, PublicGatewayError::Internal(_)) {
122            self.to_string()
123        } else {
124            sanitized
125        }
126    }
127}
128
129#[cfg(test)]
130#[path = "error/tests.rs"]
131mod tests;
132
133/// Errors that authenticated endpoints can encounter. Full error message and
134/// error details are returned to the admin client for debugging purposes.
135#[derive(Debug, Error)]
136pub enum AdminGatewayError {
137    #[error("Failed to create a federation client")]
138    ClientCreationError(anyhow::Error),
139    #[error("Failed to remove a federation client: {0}")]
140    ClientRemovalError(String),
141    #[error("There was an error with the Gateway's mnemonic: {}", .0.fmt_compact_anyhow())]
142    MnemonicError(anyhow::Error),
143    #[error("Unexpected Error: {}", .0.fmt_compact_anyhow())]
144    Unexpected(#[from] anyhow::Error),
145    #[error("{}", .0)]
146    FederationNotConnected(#[from] FederationNotConnected),
147    #[error("Error configuring the gateway: {0}")]
148    GatewayConfigurationError(String),
149    #[error("Lightning error: {}", .0)]
150    Lightning(#[from] LightningRpcError),
151    #[error("Error registering federation {federation_id}")]
152    RegistrationError { federation_id: FederationId },
153    #[error("Error withdrawing funds onchain: {failure_reason}")]
154    WithdrawError { failure_reason: String },
155}
156
157impl IntoResponse for AdminGatewayError {
158    // For admin errors, always pass along the full error message for debugging
159    // purposes
160    fn into_response(self) -> Response {
161        crit!(target: LOG_GATEWAY, "{self}");
162        Response::builder()
163            .status(StatusCode::INTERNAL_SERVER_ERROR)
164            .body(self.to_string().into())
165            .expect("Failed to create Response")
166    }
167}
168
169/// Errors that can occur during the LNv1 protocol. LNv1 errors are public and
170/// the error messages should be redacted for privacy reasons.
171#[derive(Debug, Error)]
172pub enum LNv1Error {
173    #[error("Incoming payment error: {}", OptStacktrace(.0))]
174    IncomingPayment(String),
175    #[error(
176        "Outgoing Contract Error Reason: {message} Stack: {}",
177        OptStacktrace(error)
178    )]
179    OutgoingContract {
180        error: Box<OutgoingPaymentError>,
181        message: String,
182    },
183    #[error("Outgoing Payment Error: {}", OptStacktrace(.0))]
184    OutgoingPayment(#[from] anyhow::Error),
185}
186
187/// Errors that can occur during the LNv2 protocol. LNv2 errors are public and
188/// the error messages should be redacted for privacy reasons.
189#[derive(Debug, Error)]
190pub enum LNv2Error {
191    #[error("Incoming Payment Error: {}", .0)]
192    IncomingPayment(String),
193    #[error("Outgoing Payment Error: {}", OptStacktrace(.0))]
194    OutgoingPayment(#[from] anyhow::Error),
195}
196
197/// Public error that indicates the requested federation is not connected to
198/// this gateway.
199#[derive(Debug, Error)]
200pub struct FederationNotConnected {
201    pub federation_id_prefix: FederationIdPrefix,
202}
203
204impl Display for FederationNotConnected {
205    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
206        write!(
207            f,
208            "No federation available for prefix {}",
209            self.federation_id_prefix
210        )
211    }
212}
213
214/// LNURL-compliant error response for verify endpoints
215#[derive(Debug, Error)]
216pub(crate) struct LnurlError {
217    code: StatusCode,
218    reason: anyhow::Error,
219}
220
221impl Display for LnurlError {
222    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
223        write!(f, "LNURL Error: {}", self.reason,)
224    }
225}
226
227impl LnurlError {
228    pub(crate) fn internal(reason: anyhow::Error) -> Self {
229        Self {
230            code: StatusCode::INTERNAL_SERVER_ERROR,
231            reason,
232        }
233    }
234}
235
236impl IntoResponse for LnurlError {
237    fn into_response(self) -> Response<Body> {
238        let json = Json(serde_json::json!({
239            "status": "ERROR",
240            "reason": self.reason.to_string(),
241        }));
242
243        (self.code, json).into_response()
244    }
245}