Skip to content

Commit 341eeea

Browse files
authored
Extracting the Error impl from the monolith eth2 (#7878)
Currently the `eth2` crate lib file is a large monolith of almost 3000 lines of code. As part of the bosun migration we are trying to increase code readability and modularity in the lighthouse crates initially, which then can be transferred to bosun Co-Authored-By: hopinheimer <[email protected]> Co-Authored-By: hopinheimer <[email protected]>
1 parent f4b1bb4 commit 341eeea

File tree

4 files changed

+176
-177
lines changed

4 files changed

+176
-177
lines changed

common/eth2/src/error.rs

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
//! Centralized error handling for eth2 API clients
2+
//!
3+
//! This module consolidates all error types, response processing,
4+
//! and recovery logic for both beacon node and validator client APIs.
5+
6+
use pretty_reqwest_error::PrettyReqwestError;
7+
use reqwest::{Response, StatusCode};
8+
use sensitive_url::SensitiveUrl;
9+
use serde::{Deserialize, Serialize};
10+
use std::{fmt, path::PathBuf};
11+
12+
/// Main error type for eth2 API clients
13+
#[derive(Debug)]
14+
pub enum Error {
15+
/// The `reqwest` client raised an error.
16+
HttpClient(PrettyReqwestError),
17+
/// The `reqwest_eventsource` client raised an error.
18+
SseClient(Box<reqwest_eventsource::Error>),
19+
/// The server returned an error message where the body was able to be parsed.
20+
ServerMessage(ErrorMessage),
21+
/// The server returned an error message with an array of errors.
22+
ServerIndexedMessage(IndexedErrorMessage),
23+
/// The server returned an error message where the body was unable to be parsed.
24+
StatusCode(StatusCode),
25+
/// The supplied URL is badly formatted. It should look something like `http://127.0.0.1:5052`.
26+
InvalidUrl(SensitiveUrl),
27+
/// The supplied validator client secret is invalid.
28+
InvalidSecret(String),
29+
/// The server returned a response with an invalid signature. It may be an impostor.
30+
InvalidSignatureHeader,
31+
/// The server returned a response without a signature header. It may be an impostor.
32+
MissingSignatureHeader,
33+
/// The server returned an invalid JSON response.
34+
InvalidJson(serde_json::Error),
35+
/// The server returned an invalid server-sent event.
36+
InvalidServerSentEvent(String),
37+
/// The server sent invalid response headers.
38+
InvalidHeaders(String),
39+
/// The server returned an invalid SSZ response.
40+
InvalidSsz(ssz::DecodeError),
41+
/// An I/O error occurred while loading an API token from disk.
42+
TokenReadError(PathBuf, std::io::Error),
43+
/// The client has been configured without a server pubkey, but requires one for this request.
44+
NoServerPubkey,
45+
/// The client has been configured without an API token, but requires one for this request.
46+
NoToken,
47+
}
48+
49+
/// An API error serializable to JSON.
50+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
51+
pub struct ErrorMessage {
52+
pub code: u16,
53+
pub message: String,
54+
#[serde(default)]
55+
pub stacktraces: Vec<String>,
56+
}
57+
58+
/// An indexed API error serializable to JSON.
59+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
60+
pub struct IndexedErrorMessage {
61+
pub code: u16,
62+
pub message: String,
63+
pub failures: Vec<Failure>,
64+
}
65+
66+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
67+
pub struct Failure {
68+
pub index: u64,
69+
pub message: String,
70+
}
71+
72+
impl Failure {
73+
pub fn new(index: usize, message: String) -> Self {
74+
Self {
75+
index: index as u64,
76+
message,
77+
}
78+
}
79+
}
80+
81+
/// Server error response variants
82+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
83+
#[serde(untagged)]
84+
pub enum ResponseError {
85+
Indexed(IndexedErrorMessage),
86+
Message(ErrorMessage),
87+
}
88+
89+
impl Error {
90+
/// If the error has a HTTP status code, return it.
91+
pub fn status(&self) -> Option<StatusCode> {
92+
match self {
93+
Error::HttpClient(error) => error.inner().status(),
94+
Error::SseClient(error) => {
95+
if let reqwest_eventsource::Error::InvalidStatusCode(status, _) = error.as_ref() {
96+
Some(*status)
97+
} else {
98+
None
99+
}
100+
}
101+
Error::ServerMessage(msg) => StatusCode::try_from(msg.code).ok(),
102+
Error::ServerIndexedMessage(msg) => StatusCode::try_from(msg.code).ok(),
103+
Error::StatusCode(status) => Some(*status),
104+
Error::InvalidUrl(_) => None,
105+
Error::InvalidSecret(_) => None,
106+
Error::InvalidSignatureHeader => None,
107+
Error::MissingSignatureHeader => None,
108+
Error::InvalidJson(_) => None,
109+
Error::InvalidSsz(_) => None,
110+
Error::InvalidServerSentEvent(_) => None,
111+
Error::InvalidHeaders(_) => None,
112+
Error::TokenReadError(..) => None,
113+
Error::NoServerPubkey | Error::NoToken => None,
114+
}
115+
}
116+
}
117+
118+
impl From<reqwest::Error> for Error {
119+
fn from(error: reqwest::Error) -> Self {
120+
Error::HttpClient(error.into())
121+
}
122+
}
123+
124+
impl fmt::Display for Error {
125+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
126+
write!(f, "{:?}", self)
127+
}
128+
}
129+
130+
/// Returns `Ok(response)` if the response is a `200 OK`, `202 ACCEPTED`, or `204 NO_CONTENT`
131+
/// Otherwise, creates an appropriate error message.
132+
pub async fn ok_or_error(response: Response) -> Result<Response, Error> {
133+
let status = response.status();
134+
135+
if status == StatusCode::OK
136+
|| status == StatusCode::ACCEPTED
137+
|| status == StatusCode::NO_CONTENT
138+
{
139+
Ok(response)
140+
} else if let Ok(message) = response.json::<ResponseError>().await {
141+
match message {
142+
ResponseError::Message(message) => Err(Error::ServerMessage(message)),
143+
ResponseError::Indexed(indexed) => Err(Error::ServerIndexedMessage(indexed)),
144+
}
145+
} else {
146+
Err(Error::StatusCode(status))
147+
}
148+
}
149+
150+
/// Returns `Ok(response)` if the response is a success (2xx) response. Otherwise, creates an
151+
/// appropriate error message.
152+
pub async fn success_or_error(response: Response) -> Result<Response, Error> {
153+
let status = response.status();
154+
155+
if status.is_success() {
156+
Ok(response)
157+
} else if let Ok(message) = response.json().await {
158+
match message {
159+
ResponseError::Message(message) => Err(Error::ServerMessage(message)),
160+
ResponseError::Indexed(indexed) => Err(Error::ServerIndexedMessage(indexed)),
161+
}
162+
} else {
163+
Err(Error::StatusCode(status))
164+
}
165+
}

common/eth2/src/lib.rs

Lines changed: 3 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,22 @@
77
//! Eventually it would be ideal to publish this crate on crates.io, however we have some local
88
//! dependencies preventing this presently.
99
10+
pub mod error;
1011
#[cfg(feature = "lighthouse")]
1112
pub mod lighthouse;
1213
#[cfg(feature = "lighthouse")]
1314
pub mod lighthouse_vc;
1415
pub mod mixin;
1516
pub mod types;
1617

18+
pub use self::error::{Error, ok_or_error, success_or_error};
1719
use self::mixin::{RequestAccept, ResponseOptional};
18-
use self::types::{Error as ResponseError, *};
20+
use self::types::*;
1921
use ::types::beacon_response::ExecutionOptimisticFinalizedBeaconResponse;
2022
use derivative::Derivative;
2123
use futures::Stream;
2224
use futures_util::StreamExt;
2325
use libp2p_identity::PeerId;
24-
use pretty_reqwest_error::PrettyReqwestError;
2526
pub use reqwest;
2627
use reqwest::{
2728
Body, IntoUrl, RequestBuilder, Response,
@@ -34,7 +35,6 @@ use serde::{Serialize, de::DeserializeOwned};
3435
use ssz::Encode;
3536
use std::fmt;
3637
use std::future::Future;
37-
use std::path::PathBuf;
3838
use std::time::Duration;
3939

4040
pub const V1: EndpointVersion = EndpointVersion(1);
@@ -68,83 +68,6 @@ const HTTP_GET_DEPOSIT_SNAPSHOT_QUOTIENT: u32 = 4;
6868
const HTTP_GET_VALIDATOR_BLOCK_TIMEOUT_QUOTIENT: u32 = 4;
6969
const HTTP_DEFAULT_TIMEOUT_QUOTIENT: u32 = 4;
7070

71-
#[derive(Debug)]
72-
pub enum Error {
73-
/// The `reqwest` client raised an error.
74-
HttpClient(PrettyReqwestError),
75-
/// The `reqwest_eventsource` client raised an error.
76-
SseClient(Box<reqwest_eventsource::Error>),
77-
/// The server returned an error message where the body was able to be parsed.
78-
ServerMessage(ErrorMessage),
79-
/// The server returned an error message with an array of errors.
80-
ServerIndexedMessage(IndexedErrorMessage),
81-
/// The server returned an error message where the body was unable to be parsed.
82-
StatusCode(StatusCode),
83-
/// The supplied URL is badly formatted. It should look something like `http://127.0.0.1:5052`.
84-
InvalidUrl(SensitiveUrl),
85-
/// The supplied validator client secret is invalid.
86-
InvalidSecret(String),
87-
/// The server returned a response with an invalid signature. It may be an impostor.
88-
InvalidSignatureHeader,
89-
/// The server returned a response without a signature header. It may be an impostor.
90-
MissingSignatureHeader,
91-
/// The server returned an invalid JSON response.
92-
InvalidJson(serde_json::Error),
93-
/// The server returned an invalid server-sent event.
94-
InvalidServerSentEvent(String),
95-
/// The server sent invalid response headers.
96-
InvalidHeaders(String),
97-
/// The server returned an invalid SSZ response.
98-
InvalidSsz(ssz::DecodeError),
99-
/// An I/O error occurred while loading an API token from disk.
100-
TokenReadError(PathBuf, std::io::Error),
101-
/// The client has been configured without a server pubkey, but requires one for this request.
102-
NoServerPubkey,
103-
/// The client has been configured without an API token, but requires one for this request.
104-
NoToken,
105-
}
106-
107-
impl From<reqwest::Error> for Error {
108-
fn from(error: reqwest::Error) -> Self {
109-
Error::HttpClient(error.into())
110-
}
111-
}
112-
113-
impl Error {
114-
/// If the error has a HTTP status code, return it.
115-
pub fn status(&self) -> Option<StatusCode> {
116-
match self {
117-
Error::HttpClient(error) => error.inner().status(),
118-
Error::SseClient(error) => {
119-
if let reqwest_eventsource::Error::InvalidStatusCode(status, _) = error.as_ref() {
120-
Some(*status)
121-
} else {
122-
None
123-
}
124-
}
125-
Error::ServerMessage(msg) => StatusCode::try_from(msg.code).ok(),
126-
Error::ServerIndexedMessage(msg) => StatusCode::try_from(msg.code).ok(),
127-
Error::StatusCode(status) => Some(*status),
128-
Error::InvalidUrl(_) => None,
129-
Error::InvalidSecret(_) => None,
130-
Error::InvalidSignatureHeader => None,
131-
Error::MissingSignatureHeader => None,
132-
Error::InvalidJson(_) => None,
133-
Error::InvalidSsz(_) => None,
134-
Error::InvalidServerSentEvent(_) => None,
135-
Error::InvalidHeaders(_) => None,
136-
Error::TokenReadError(..) => None,
137-
Error::NoServerPubkey | Error::NoToken => None,
138-
}
139-
}
140-
}
141-
142-
impl fmt::Display for Error {
143-
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
144-
write!(f, "{:?}", self)
145-
}
146-
}
147-
14871
/// A struct to define a variety of different timeouts for different validator tasks to ensure
14972
/// proper fallback behaviour.
15073
#[derive(Clone, Debug, PartialEq, Eq)]
@@ -2928,37 +2851,3 @@ impl BeaconNodeHttpClient {
29282851
.await
29292852
}
29302853
}
2931-
2932-
/// Returns `Ok(response)` if the response is a `200 OK` response. Otherwise, creates an
2933-
/// appropriate error message.
2934-
pub async fn ok_or_error(response: Response) -> Result<Response, Error> {
2935-
let status = response.status();
2936-
2937-
if status == StatusCode::OK {
2938-
Ok(response)
2939-
} else if let Ok(message) = response.json().await {
2940-
match message {
2941-
ResponseError::Message(message) => Err(Error::ServerMessage(message)),
2942-
ResponseError::Indexed(indexed) => Err(Error::ServerIndexedMessage(indexed)),
2943-
}
2944-
} else {
2945-
Err(Error::StatusCode(status))
2946-
}
2947-
}
2948-
2949-
/// Returns `Ok(response)` if the response is a success (2xx) response. Otherwise, creates an
2950-
/// appropriate error message.
2951-
pub async fn success_or_error(response: Response) -> Result<Response, Error> {
2952-
let status = response.status();
2953-
2954-
if status.is_success() {
2955-
Ok(response)
2956-
} else if let Ok(message) = response.json().await {
2957-
match message {
2958-
ResponseError::Message(message) => Err(Error::ServerMessage(message)),
2959-
ResponseError::Indexed(indexed) => Err(Error::ServerIndexedMessage(indexed)),
2960-
}
2961-
} else {
2962-
Err(Error::StatusCode(status))
2963-
}
2964-
}

common/eth2/src/lighthouse_vc/http_client.rs

Lines changed: 6 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use super::types::*;
2-
use crate::Error;
2+
use crate::{Error, success_or_error};
33
use reqwest::{
44
IntoUrl,
55
header::{HeaderMap, HeaderValue},
@@ -145,7 +145,7 @@ impl ValidatorClientHttpClient {
145145
.send()
146146
.await
147147
.map_err(Error::from)?;
148-
ok_or_error(response).await
148+
success_or_error(response).await
149149
}
150150

151151
/// Perform a HTTP DELETE request, returning the `Response` for further processing.
@@ -157,7 +157,7 @@ impl ValidatorClientHttpClient {
157157
.send()
158158
.await
159159
.map_err(Error::from)?;
160-
ok_or_error(response).await
160+
success_or_error(response).await
161161
}
162162

163163
async fn get<T: DeserializeOwned, U: IntoUrl>(&self, url: U) -> Result<T, Error> {
@@ -218,7 +218,7 @@ impl ValidatorClientHttpClient {
218218
.send()
219219
.await
220220
.map_err(Error::from)?;
221-
ok_or_error(response).await
221+
success_or_error(response).await
222222
}
223223

224224
async fn post<T: Serialize, U: IntoUrl, V: DeserializeOwned>(
@@ -250,7 +250,7 @@ impl ValidatorClientHttpClient {
250250
.send()
251251
.await
252252
.map_err(Error::from)?;
253-
ok_or_error(response).await?;
253+
success_or_error(response).await?;
254254
Ok(())
255255
}
256256

@@ -268,7 +268,7 @@ impl ValidatorClientHttpClient {
268268
.send()
269269
.await
270270
.map_err(Error::from)?;
271-
ok_or_error(response).await
271+
success_or_error(response).await
272272
}
273273

274274
/// Perform a HTTP DELETE request.
@@ -681,20 +681,3 @@ impl ValidatorClientHttpClient {
681681
self.delete(url).await
682682
}
683683
}
684-
685-
/// Returns `Ok(response)` if the response is a `200 OK` response or a
686-
/// `202 Accepted` response. Otherwise, creates an appropriate error message.
687-
async fn ok_or_error(response: Response) -> Result<Response, Error> {
688-
let status = response.status();
689-
690-
if status == StatusCode::OK
691-
|| status == StatusCode::ACCEPTED
692-
|| status == StatusCode::NO_CONTENT
693-
{
694-
Ok(response)
695-
} else if let Ok(message) = response.json().await {
696-
Err(Error::ServerMessage(message))
697-
} else {
698-
Err(Error::StatusCode(status))
699-
}
700-
}

0 commit comments

Comments
 (0)