Skip to content

Commit d851595

Browse files
authored
Merge pull request #22 from interledger/17-update-error-types-to-match-other-sdks
17 update error types to match other sdks
2 parents 4b15f54 + a3a89a8 commit d851595

File tree

10 files changed

+283
-146
lines changed

10 files changed

+283
-146
lines changed

src/client/core.rs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,19 +73,23 @@ impl AuthenticatedOpenPaymentsClient {
7373
///
7474
/// # Errors
7575
///
76-
/// - `OpClientError::Signature` if the signing key cannot be loaded or generated
77-
/// - `OpClientError::Signature` if JWKS cannot be saved to the specified path
76+
/// Returns an `OpClientError` with:
77+
/// - `description`: Human-readable error message
78+
/// - `status`: HTTP status text (for HTTP errors)
79+
/// - `code`: HTTP status code (for HTTP errors)
80+
/// - `validation_errors`: List of validation errors (if applicable)
81+
/// - `details`: Additional error details (if applicable)
7882
pub fn new(config: ClientConfig) -> Result<Self> {
7983
let http_client = ReqwestClient::new();
8084

8185
let signing_key = load_or_generate_key(&config.private_key_path).map_err(|e| {
82-
OpClientError::Signature(format!("Failed to load or generate signing key: {e}"))
86+
OpClientError::signature(format!("Failed to load or generate signing key: {e}"))
8387
})?;
8488

8589
if let Some(ref jwks_path) = config.jwks_path {
8690
let jwks_json = Jwk::generate_jwks_json(&signing_key, &config.key_id);
8791
Jwk::save_jwks(&jwks_json, jwks_path).map_err(|e| {
88-
OpClientError::Signature(format!("Failed to save JWK to file: {e}"))
92+
OpClientError::signature(format!("Failed to save JWK to file: {e}"))
8993
})?;
9094
}
9195

src/client/error.rs

Lines changed: 201 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@
44
//! All client operations return a [`Result<T, OpClientError>`] which provides
55
//! detailed error information for different failure scenarios.
66
//!
7-
//! ## Error Categories
7+
//! ## Error Structure
88
//!
9-
//! - **HTTP Errors**: Network and HTTP protocol errors
10-
//! - **Parsing Errors**: Header, URL, and data parsing failures
11-
//! - **Cryptographic Errors**: Key and signature-related issues
12-
//! - **I/O Errors**: File system and network I/O problems
9+
//! - `description` - Human-readable error message
10+
//! - `validationErrors` - Optional list of validation error messages
11+
//! - `status` - HTTP status code (only for HTTP errors)
12+
//! - `code` - Error code (only for HTTP errors)
13+
//! - `details` - Additional error details as key-value pairs
1314
//!
1415
//! ## Example Usage
1516
//!
@@ -19,118 +20,218 @@
1920
//! fn handle_client_error(result: Result<()>) {
2021
//! match result {
2122
//! Ok(()) => println!("Operation successful"),
22-
//! Err(OpClientError::Http(msg)) => eprintln!("HTTP error: {}", msg),
23-
//! Err(OpClientError::Signature(msg)) => eprintln!("Signature error: {}", msg),
24-
//! Err(OpClientError::Other(msg)) => eprintln!("Other error: {}", msg),
25-
//! Err(e) => eprintln!("Unexpected error: {:?}", e),
23+
//! Err(e) => {
24+
//! eprintln!("Error: {}", e.description);
25+
//! if let Some(status) = e.status {
26+
//! eprintln!("Status: {}", status);
27+
//! }
28+
//! if let Some(code) = e.code {
29+
//! eprintln!("Code: {}", code);
30+
//! }
31+
//! if let Some(validation_errors) = e.validation_errors {
32+
//! for error in validation_errors {
33+
//! eprintln!("Validation error: {}", error);
34+
//! }
35+
//! }
36+
//! }
2637
//! }
2738
//! }
2839
//! ```
2940
41+
use std::collections::HashMap;
3042
use thiserror::Error;
3143

3244
/// Error type for Open Payments client operations.
3345
///
34-
/// This enum provides detailed error information for different types of failures
35-
/// that can occur during client operations. Each variant includes context-specific
36-
/// error messages to help with debugging and error handling.
46+
/// ## Fields
3747
///
38-
/// ## Error Variants
39-
///
40-
/// - `Http` - Network and HTTP protocol errors
41-
/// - `HeaderParse` - HTTP header parsing failures
42-
/// - `Serde` - JSON serialization/deserialization errors
43-
/// - `Io` - File system and I/O errors
44-
/// - `Pem` - PEM format parsing errors
45-
/// - `Pkcs8` - PKCS8 key format errors
46-
/// - `Base64` - Base64 encoding/decoding errors
47-
/// - `Url` - URL parsing errors
48-
/// - `Signature` - Cryptographic signature errors
49-
/// - `Other` - Miscellaneous errors
48+
/// - `description` - Human-readable error message
49+
/// - `validation_errors` - Optional list of validation error messages
50+
/// - `status` - HTTP status code (only relevant for HTTP errors)
51+
/// - `code` - Error code (only relevant for HTTP errors)
52+
/// - `details` - Additional error details as key-value pairs
5053
#[derive(Debug, Error)]
51-
pub enum OpClientError {
52-
/// HTTP protocol or network-related errors.
53-
///
54-
/// This includes connection failures, timeout errors, and HTTP status code errors.
55-
/// The error message provides details about the specific HTTP issue.
56-
#[error("HTTP error: {0}")]
57-
Http(String),
58-
59-
/// HTTP header parsing errors.
60-
///
61-
/// Occurs when the client cannot parse required HTTP headers such as
62-
/// `Content-Type`, `Authorization`, or custom headers.
63-
#[error("Header parse error: {0}")]
64-
HeaderParse(String),
65-
66-
/// JSON serialization or deserialization errors.
67-
///
68-
/// This error is automatically converted from `serde_json::Error` and occurs
69-
/// when the client cannot serialize request data or deserialize response data.
70-
#[error("Serde error: {0}")]
71-
Serde(#[from] serde_json::Error),
72-
73-
/// File system and I/O errors.
74-
///
75-
/// This error is automatically converted from `std::io::Error` and occurs
76-
/// when the client cannot read key files or perform other I/O operations.
77-
#[error("IO error: {0}")]
78-
Io(#[from] std::io::Error),
79-
80-
/// PEM format parsing errors.
81-
///
82-
/// Occurs when the client cannot parse PEM-encoded private keys or certificates.
83-
/// This includes malformed PEM files or unsupported PEM types.
84-
#[error("Invalid PEM: {0}")]
85-
Pem(String),
86-
87-
/// PKCS8 key format errors.
88-
///
89-
/// Occurs when the client cannot parse PKCS8-encoded private keys.
90-
/// This includes unsupported key algorithms or malformed key data.
91-
#[error("PKCS8 error: {0}")]
92-
Pkcs8(String),
93-
94-
/// Base64 encoding or decoding errors.
95-
///
96-
/// This error is automatically converted from `base64::DecodeError` and occurs
97-
/// when the client cannot decode Base64-encoded data such as signatures or keys.
98-
#[error("Base64 error: {0}")]
99-
Base64(#[from] base64::DecodeError),
100-
101-
/// URL parsing errors.
102-
///
103-
/// This error is automatically converted from `url::ParseError` and occurs
104-
/// when the client cannot parse URLs for wallet addresses or API endpoints.
105-
#[error("URL parse error: {0}")]
106-
Url(#[from] url::ParseError),
107-
108-
/// Cryptographic signature errors.
109-
///
110-
/// Occurs when there are issues with HTTP message signature creation or validation.
111-
/// This includes key loading failures, signature generation errors, and validation failures.
112-
#[error("Signature error: {0}")]
113-
Signature(String),
114-
115-
/// Miscellaneous errors that don't fit into other categories.
116-
///
117-
/// This variant is used for unexpected errors that don't fall into the standard error categories.
118-
#[error("Other error: {0}")]
119-
Other(String),
54+
pub struct OpClientError {
55+
/// Human-readable error description.
56+
pub description: String,
57+
58+
/// Optional list of validation error messages.
59+
pub validation_errors: Option<Vec<String>>,
60+
61+
/// HTTP status (only relevant for HTTP errors).
62+
pub status: Option<String>,
63+
64+
/// Error code (only relevant for HTTP errors).
65+
pub code: Option<u16>,
66+
67+
/// Additional error details as key-value pairs.
68+
pub details: Option<HashMap<String, serde_json::Value>>,
69+
}
70+
71+
impl OpClientError {
72+
/// Creates a new HTTP error with status code and optional error code.
73+
pub fn http(description: impl Into<String>, status: Option<String>, code: Option<u16>) -> Self {
74+
Self {
75+
description: description.into(),
76+
validation_errors: None,
77+
status,
78+
code,
79+
details: None,
80+
}
81+
}
82+
83+
/// Creates a new validation error with a list of validation messages.
84+
pub fn validation(description: impl Into<String>, validation_errors: Vec<String>) -> Self {
85+
Self {
86+
description: description.into(),
87+
validation_errors: Some(validation_errors),
88+
status: None,
89+
code: None,
90+
details: None,
91+
}
92+
}
93+
94+
/// Creates a new general error without HTTP-specific fields.
95+
pub fn other(description: impl Into<String>) -> Self {
96+
Self {
97+
description: description.into(),
98+
validation_errors: None,
99+
status: None,
100+
code: None,
101+
details: None,
102+
}
103+
}
104+
105+
/// Adds additional details to the error.
106+
pub fn with_details(mut self, details: HashMap<String, serde_json::Value>) -> Self {
107+
self.details = Some(details);
108+
self
109+
}
110+
111+
/// Adds a single detail key-value pair to the error.
112+
pub fn with_detail(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
113+
if self.details.is_none() {
114+
self.details = Some(HashMap::new());
115+
}
116+
if let Some(ref mut details) = self.details {
117+
details.insert(key.into(), value);
118+
}
119+
self
120+
}
121+
}
122+
123+
impl std::fmt::Display for OpClientError {
124+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
125+
write!(f, "{}", self.description)?;
126+
127+
if let Some(status) = &self.status {
128+
write!(f, " (Status: {status})")?;
129+
}
130+
131+
if let Some(code) = &self.code {
132+
write!(f, " (Code: {code})")?;
133+
}
134+
135+
if let Some(validation_errors) = &self.validation_errors {
136+
write!(f, " [Validation errors: {}]", validation_errors.join(", "))?;
137+
}
138+
139+
Ok(())
140+
}
120141
}
121142

122143
impl From<reqwest::Error> for OpClientError {
123-
/// Converts reqwest HTTP errors to `OpClientError::Http`.
124-
///
125-
/// This implementation allows the client to automatically convert reqwest errors
126-
/// into the unified error type, providing consistent error handling across the library.
127144
fn from(err: reqwest::Error) -> Self {
128-
OpClientError::Http(format!("{err}"))
145+
let status = err
146+
.status()
147+
.map(|s| s.canonical_reason().unwrap_or("Unknown").to_string());
148+
let code = err.status().map(|s| s.as_u16());
149+
let description = format!("HTTP error: {err}");
150+
151+
Self {
152+
description,
153+
validation_errors: None,
154+
status,
155+
code,
156+
details: None,
157+
}
158+
}
159+
}
160+
161+
impl From<serde_json::Error> for OpClientError {
162+
fn from(err: serde_json::Error) -> Self {
163+
Self::other(format!("JSON serialization/deserialization error: {err}"))
164+
}
165+
}
166+
167+
impl From<std::io::Error> for OpClientError {
168+
fn from(err: std::io::Error) -> Self {
169+
Self::other(format!("I/O error: {err}"))
170+
}
171+
}
172+
173+
impl From<base64::DecodeError> for OpClientError {
174+
fn from(err: base64::DecodeError) -> Self {
175+
Self::other(format!("Base64 decoding error: {err}"))
176+
}
177+
}
178+
179+
impl From<url::ParseError> for OpClientError {
180+
fn from(err: url::ParseError) -> Self {
181+
Self::other(format!("URL parsing error: {err}"))
182+
}
183+
}
184+
185+
impl OpClientError {
186+
pub fn header_parse(description: impl Into<String>) -> Self {
187+
Self::other(format!("Header parse error: {}", description.into()))
188+
}
189+
190+
pub fn pem(description: impl Into<String>) -> Self {
191+
Self::other(format!("Invalid PEM: {}", description.into()))
192+
}
193+
194+
pub fn pkcs8(description: impl Into<String>) -> Self {
195+
Self::other(format!("PKCS8 error: {}", description.into()))
196+
}
197+
198+
pub fn signature(description: impl Into<String>) -> Self {
199+
Self::other(format!("Signature error: {}", description.into()))
200+
}
201+
}
202+
203+
impl From<url::ParseError> for Box<OpClientError> {
204+
fn from(err: url::ParseError) -> Self {
205+
Box::new(OpClientError::from(err))
206+
}
207+
}
208+
209+
impl From<reqwest::Error> for Box<OpClientError> {
210+
fn from(err: reqwest::Error) -> Self {
211+
Box::new(OpClientError::from(err))
212+
}
213+
}
214+
215+
impl From<serde_json::Error> for Box<OpClientError> {
216+
fn from(err: serde_json::Error) -> Self {
217+
Box::new(OpClientError::from(err))
218+
}
219+
}
220+
221+
impl From<std::io::Error> for Box<OpClientError> {
222+
fn from(err: std::io::Error) -> Self {
223+
Box::new(OpClientError::from(err))
224+
}
225+
}
226+
227+
impl From<base64::DecodeError> for Box<OpClientError> {
228+
fn from(err: base64::DecodeError) -> Self {
229+
Box::new(OpClientError::from(err))
129230
}
130231
}
131232

132233
/// Result type for Open Payments client operations.
133234
///
134-
/// This is a type alias for `Result<T, OpClientError>` that provides a convenient
235+
/// This is a type alias for `Result<T, Box<OpClientError>>` that provides a convenient
135236
/// way to handle client operation results with detailed error information.
136-
pub type Result<T> = std::result::Result<T, OpClientError>;
237+
pub type Result<T> = std::result::Result<T, Box<OpClientError>>;

src/client/grant.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ pub(crate) async fn request_grant(
1414
client: client.config.wallet_address_url.clone(),
1515
..grant.clone()
1616
};
17-
let body = serde_json::to_string(&grant_with_client).map_err(OpClientError::Serde)?;
17+
let body = serde_json::to_string(&grant_with_client).map_err(OpClientError::from)?;
1818

1919
AuthenticatedRequest::new(client, Method::POST, auth_url.to_string())
2020
.with_body(body)
@@ -31,7 +31,7 @@ pub(crate) async fn continue_grant(
3131
let body = serde_json::to_string(&ContinueRequest {
3232
interact_ref: Some(interact_ref.to_string()),
3333
})
34-
.map_err(OpClientError::Serde)?;
34+
.map_err(OpClientError::from)?;
3535

3636
AuthenticatedRequest::new(client, Method::POST, continue_uri.to_string())
3737
.with_body(body)

src/client/payments.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ pub(crate) async fn create_incoming_payment(
1717
access_token: Option<&str>,
1818
) -> Result<IncomingPayment> {
1919
let url = join_url_paths(resource_server_url, "incoming-payments")?;
20-
let body = serde_json::to_string(req_body).map_err(OpClientError::Serde)?;
20+
let body = serde_json::to_string(req_body).map_err(OpClientError::from)?;
2121

2222
AuthenticatedRequest::new(client, Method::POST, url)
2323
.with_body(body)
@@ -84,7 +84,7 @@ pub(crate) async fn create_outgoing_payment(
8484
access_token: Option<&str>,
8585
) -> Result<OutgoingPayment> {
8686
let url = join_url_paths(resource_server_url, "outgoing-payments")?;
87-
let body = serde_json::to_string(req_body).map_err(OpClientError::Serde)?;
87+
let body = serde_json::to_string(req_body).map_err(OpClientError::from)?;
8888

8989
AuthenticatedRequest::new(client, Method::POST, url)
9090
.with_body(body)

0 commit comments

Comments
 (0)