Files
lighthouse/common/eth2/src/error.rs
Mac L 7bfcc03520 Reduce eth2 dependency space (#8524)
Remove certain dependencies from `eth2`, and feature-gate others which are only used by certain endpoints.

| Removed | Optional | Dev only |
| -------- | -------- | -------- |
| `either` `enr` `libp2p-identity` `multiaddr` | `protoarray` `eth2_keystore` `eip_3076` `zeroize` `reqwest-eventsource` `futures` `futures-util` | `rand` `test_random_derive` |

This is done by adding an `events` feature which enables the events endpoint and its associated dependencies.
The `lighthouse` feature also enables its associated dependencies making them optional.

The networking-adjacent dependencies were removed by just having certain fields use a `String` instead of an explicit network type. This means the user should handle conversion at the call site instead. This is a bit spicy, but I believe `PeerId`, `Enr` and `Multiaddr` are easily converted to and from `String`s so I think it's fine and reduces our dependency space by a lot. The alternative is to feature gate these types behind a `network` feature instead.


Co-Authored-By: Mac L <mjladson@pm.me>
2025-12-08 05:37:23 +00:00

168 lines
5.8 KiB
Rust

//! Centralized error handling for eth2 API clients
//!
//! This module consolidates all error types, response processing,
//! and recovery logic for both beacon node and validator client APIs.
use pretty_reqwest_error::PrettyReqwestError;
use reqwest::{Response, StatusCode};
use sensitive_url::SensitiveUrl;
use serde::{Deserialize, Serialize};
use std::{fmt, path::PathBuf};
/// Main error type for eth2 API clients
#[derive(Debug)]
pub enum Error {
/// The `reqwest` client raised an error.
HttpClient(PrettyReqwestError),
#[cfg(feature = "events")]
/// The `reqwest_eventsource` client raised an error.
SseClient(Box<reqwest_eventsource::Error>),
/// The server returned an error message where the body was able to be parsed.
ServerMessage(ErrorMessage),
/// The server returned an error message with an array of errors.
ServerIndexedMessage(IndexedErrorMessage),
/// The server returned an error message where the body was unable to be parsed.
StatusCode(StatusCode),
/// The supplied URL is badly formatted. It should look something like `http://127.0.0.1:5052`.
InvalidUrl(SensitiveUrl),
/// The supplied validator client secret is invalid.
InvalidSecret(String),
/// The server returned a response with an invalid signature. It may be an impostor.
InvalidSignatureHeader,
/// The server returned a response without a signature header. It may be an impostor.
MissingSignatureHeader,
/// The server returned an invalid JSON response.
InvalidJson(serde_json::Error),
/// The server returned an invalid server-sent event.
InvalidServerSentEvent(String),
/// The server sent invalid response headers.
InvalidHeaders(String),
/// The server returned an invalid SSZ response.
InvalidSsz(ssz::DecodeError),
/// An I/O error occurred while loading an API token from disk.
TokenReadError(PathBuf, std::io::Error),
/// The client has been configured without a server pubkey, but requires one for this request.
NoServerPubkey,
/// The client has been configured without an API token, but requires one for this request.
NoToken,
}
/// An API error serializable to JSON.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ErrorMessage {
pub code: u16,
pub message: String,
#[serde(default)]
pub stacktraces: Vec<String>,
}
/// An indexed API error serializable to JSON.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct IndexedErrorMessage {
pub code: u16,
pub message: String,
pub failures: Vec<Failure>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Failure {
pub index: u64,
pub message: String,
}
impl Failure {
pub fn new(index: usize, message: String) -> Self {
Self {
index: index as u64,
message,
}
}
}
/// Server error response variants
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ResponseError {
Indexed(IndexedErrorMessage),
Message(ErrorMessage),
}
impl Error {
/// If the error has a HTTP status code, return it.
pub fn status(&self) -> Option<StatusCode> {
match self {
Error::HttpClient(error) => error.inner().status(),
#[cfg(feature = "events")]
Error::SseClient(error) => {
if let reqwest_eventsource::Error::InvalidStatusCode(status, _) = error.as_ref() {
Some(*status)
} else {
None
}
}
Error::ServerMessage(msg) => StatusCode::try_from(msg.code).ok(),
Error::ServerIndexedMessage(msg) => StatusCode::try_from(msg.code).ok(),
Error::StatusCode(status) => Some(*status),
Error::InvalidUrl(_) => None,
Error::InvalidSecret(_) => None,
Error::InvalidSignatureHeader => None,
Error::MissingSignatureHeader => None,
Error::InvalidJson(_) => None,
Error::InvalidSsz(_) => None,
Error::InvalidServerSentEvent(_) => None,
Error::InvalidHeaders(_) => None,
Error::TokenReadError(..) => None,
Error::NoServerPubkey | Error::NoToken => None,
}
}
}
impl From<reqwest::Error> for Error {
fn from(error: reqwest::Error) -> Self {
Error::HttpClient(error.into())
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", self)
}
}
/// Returns `Ok(response)` if the response is a `200 OK`, `202 ACCEPTED`, or `204 NO_CONTENT`
/// Otherwise, creates an appropriate error message.
pub async fn ok_or_error(response: Response) -> Result<Response, Error> {
let status = response.status();
if status == StatusCode::OK
|| status == StatusCode::ACCEPTED
|| status == StatusCode::NO_CONTENT
{
Ok(response)
} else if let Ok(message) = response.json::<ResponseError>().await {
match message {
ResponseError::Message(message) => Err(Error::ServerMessage(message)),
ResponseError::Indexed(indexed) => Err(Error::ServerIndexedMessage(indexed)),
}
} else {
Err(Error::StatusCode(status))
}
}
/// Returns `Ok(response)` if the response is a success (2xx) response. Otherwise, creates an
/// appropriate error message.
pub async fn success_or_error(response: Response) -> Result<Response, Error> {
let status = response.status();
if status.is_success() {
Ok(response)
} else if let Ok(message) = response.json().await {
match message {
ResponseError::Message(message) => Err(Error::ServerMessage(message)),
ResponseError::Indexed(indexed) => Err(Error::ServerIndexedMessage(indexed)),
}
} else {
Err(Error::StatusCode(status))
}
}