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::FmtCompact;
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("Receiving payments is disabled for federation {federation_id_prefix}")]
61    ReceiveDisabled {
62        federation_id_prefix: FederationIdPrefix,
63    },
64    #[error("Unexpected Error: {}", OptStacktrace(.0))]
65    Unexpected(#[from] anyhow::Error),
66}
67
68impl IntoResponse for PublicGatewayError {
69    fn into_response(self) -> Response {
70        // For privacy reasons, we do not return too many details about the failure of
71        // the request back to the client to prevent malicious clients from
72        // deducing state about the gateway/lightning node.
73        //
74        // Rate limit rejections are the one error a flood of requests emits at
75        // volume, so they are logged at debug level to keep them from spamming
76        // the log.
77        if matches!(self, PublicGatewayError::RateLimited) {
78            debug!(target: LOG_GATEWAY, "{self}");
79        } else {
80            crit!(target: LOG_GATEWAY, "{self}");
81        }
82        let (error_message, status_code) = match &self {
83            PublicGatewayError::Internal(_) => (
84                "Gateway internal error".to_owned(),
85                StatusCode::INTERNAL_SERVER_ERROR,
86            ),
87            PublicGatewayError::FederationNotConnected(e) => {
88                (e.to_string(), StatusCode::BAD_REQUEST)
89            }
90            PublicGatewayError::ReceiveEcashError { .. } => (
91                "Failed to receive ecash".to_string(),
92                StatusCode::INTERNAL_SERVER_ERROR,
93            ),
94            PublicGatewayError::Lightning(_) => (
95                "Lightning Network operation failed".to_string(),
96                StatusCode::INTERNAL_SERVER_ERROR,
97            ),
98            PublicGatewayError::LNv1(_) => (
99                "LNv1 operation failed, please contact gateway operator".to_string(),
100                StatusCode::INTERNAL_SERVER_ERROR,
101            ),
102            PublicGatewayError::LNv2(_) => (
103                "LNv2 operation failed, please contact gateway operator".to_string(),
104                StatusCode::INTERNAL_SERVER_ERROR,
105            ),
106            PublicGatewayError::RateLimited => (
107                "Too many requests, please try again later".to_string(),
108                StatusCode::TOO_MANY_REQUESTS,
109            ),
110            // The policy is public: LNv2 clients learn it from `routing_info`
111            // anyway, so naming the federation here reveals nothing new.
112            PublicGatewayError::ReceiveDisabled { .. } => {
113                (self.to_string(), StatusCode::BAD_REQUEST)
114            }
115            PublicGatewayError::Unexpected(e) => (e.to_string(), StatusCode::BAD_REQUEST),
116        };
117
118        let error_message =
119            self.response_message(error_message, is_env_var_set(FM_DEBUG_GATEWAY_ENV));
120
121        Response::builder()
122            .status(status_code)
123            .body(error_message.into())
124            .expect("Failed to create Response")
125    }
126}
127
128impl PublicGatewayError {
129    fn response_message(&self, sanitized: String, expose_debug_details: bool) -> String {
130        if expose_debug_details && !matches!(self, PublicGatewayError::Internal(_)) {
131            self.to_string()
132        } else {
133            sanitized
134        }
135    }
136}
137
138#[cfg(test)]
139#[path = "error/tests.rs"]
140mod tests;
141
142/// Errors that authenticated endpoints can encounter. Full error message and
143/// error details are returned to the admin client for debugging purposes.
144#[derive(Debug, Error)]
145pub enum AdminGatewayError {
146    #[error("Failed to create a federation client")]
147    ClientCreationError(anyhow::Error),
148    #[error("Failed to remove a federation client: {0}")]
149    ClientRemovalError(String),
150    #[error("There was an error with the Gateway's mnemonic: {}", .0.fmt_compact())]
151    MnemonicError(anyhow::Error),
152    #[error("Unexpected Error: {}", .0.fmt_compact())]
153    Unexpected(#[from] anyhow::Error),
154    #[error("{}", .0)]
155    FederationNotConnected(#[from] FederationNotConnected),
156    #[error("Error configuring the gateway: {0}")]
157    GatewayConfigurationError(String),
158    #[error("Lightning error: {}", .0)]
159    Lightning(#[from] LightningRpcError),
160    #[error("Error registering federation {federation_id}")]
161    RegistrationError { federation_id: FederationId },
162    #[error("Error withdrawing funds onchain: {failure_reason}")]
163    WithdrawError { failure_reason: String },
164}
165
166impl IntoResponse for AdminGatewayError {
167    // For admin errors, always pass along the full error message for debugging
168    // purposes
169    fn into_response(self) -> Response {
170        crit!(target: LOG_GATEWAY, "{self}");
171        Response::builder()
172            .status(StatusCode::INTERNAL_SERVER_ERROR)
173            .body(self.to_string().into())
174            .expect("Failed to create Response")
175    }
176}
177
178/// Errors that can occur during the LNv1 protocol. LNv1 errors are public and
179/// the error messages should be redacted for privacy reasons.
180#[derive(Debug, Error)]
181pub enum LNv1Error {
182    #[error("Incoming payment error: {}", OptStacktrace(.0))]
183    IncomingPayment(String),
184    #[error(
185        "Outgoing Contract Error Reason: {message} Stack: {}",
186        OptStacktrace(error)
187    )]
188    OutgoingContract {
189        error: Box<OutgoingPaymentError>,
190        message: String,
191    },
192    #[error("Outgoing Payment Error: {}", OptStacktrace(.0))]
193    OutgoingPayment(#[from] anyhow::Error),
194}
195
196/// Errors that can occur during the LNv2 protocol. LNv2 errors are public and
197/// the error messages should be redacted for privacy reasons.
198#[derive(Debug, Error)]
199pub enum LNv2Error {
200    #[error("Incoming Payment Error: {}", .0)]
201    IncomingPayment(String),
202    #[error("Outgoing Payment Error: {}", OptStacktrace(.0))]
203    OutgoingPayment(#[from] anyhow::Error),
204}
205
206/// Public error that indicates the requested federation is not connected to
207/// this gateway.
208#[derive(Debug, Error)]
209pub struct FederationNotConnected {
210    pub federation_id_prefix: FederationIdPrefix,
211}
212
213impl Display for FederationNotConnected {
214    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
215        write!(
216            f,
217            "No federation available for prefix {}",
218            self.federation_id_prefix
219        )
220    }
221}
222
223/// LNURL-compliant error response for verify endpoints
224#[derive(Debug, Error)]
225pub(crate) struct LnurlError {
226    code: StatusCode,
227    reason: anyhow::Error,
228}
229
230impl Display for LnurlError {
231    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
232        write!(f, "LNURL Error: {}", self.reason,)
233    }
234}
235
236impl LnurlError {
237    pub(crate) fn internal(reason: anyhow::Error) -> Self {
238        Self {
239            code: StatusCode::INTERNAL_SERVER_ERROR,
240            reason,
241        }
242    }
243}
244
245impl IntoResponse for LnurlError {
246    fn into_response(self) -> Response<Body> {
247        let json = Json(serde_json::json!({
248            "status": "ERROR",
249            "reason": self.reason.to_string(),
250        }));
251
252        (self.code, json).into_response()
253    }
254}