shopify_sdk/clients/errors.rs
1//! HTTP-specific error types for the Shopify API SDK.
2//!
3//! This module contains error types for HTTP operations, including response
4//! errors, retry exhaustion, and request validation failures.
5//!
6//! # Error Handling
7//!
8//! The SDK uses specific error types for different failure scenarios:
9//!
10//! - [`HttpResponseError`]: Non-2xx HTTP responses from the API
11//! - [`MaxHttpRetriesExceededError`]: When retry attempts are exhausted
12//! - [`InvalidHttpRequestError`]: When a request fails validation before sending
13//! - [`HttpError`]: Unified error type encompassing all HTTP-related errors
14//!
15//! # Example
16//!
17//! ```rust,ignore
18//! use shopify_sdk::clients::{HttpClient, HttpRequest, HttpMethod, HttpError};
19//!
20//! match client.request(request).await {
21//! Ok(response) => println!("Success: {}", response.body),
22//! Err(HttpError::Response(e)) => {
23//! println!("API error {}: {}", e.code, e.message);
24//! }
25//! Err(HttpError::MaxRetries(e)) => {
26//! println!("Retries exhausted after {} tries", e.tries);
27//! }
28//! Err(HttpError::InvalidRequest(e)) => {
29//! println!("Invalid request: {}", e);
30//! }
31//! Err(HttpError::Network(e)) => {
32//! println!("Network error: {}", e);
33//! }
34//! }
35//! ```
36
37use thiserror::Error;
38
39/// Error returned when an HTTP request receives a non-successful response.
40///
41/// This error includes the status code and a serialized error message in JSON
42/// format matching the Ruby SDK's `serialized_error()` output.
43///
44/// # JSON Message Format
45///
46/// The message field contains JSON with any of these fields from the response:
47/// - `errors`: Array of error messages
48/// - `error`: Single error message
49/// - `error_description`: Description of the error
50/// - `error_reference`: Debugging reference including X-Request-Id
51///
52/// # Example
53///
54/// ```rust
55/// use shopify_sdk::clients::HttpResponseError;
56///
57/// let error = HttpResponseError {
58/// code: 404,
59/// message: r#"{"error":"Not found"}"#.to_string(),
60/// error_reference: Some("abc-123".to_string()),
61/// };
62///
63/// println!("Status {}: {}", error.code, error.message);
64/// ```
65#[derive(Debug, Error)]
66#[error("{message}")]
67pub struct HttpResponseError {
68 /// The HTTP status code of the response.
69 pub code: u16,
70 /// Serialized error message in JSON format.
71 pub message: String,
72 /// Reference ID for error reporting (from X-Request-Id header).
73 pub error_reference: Option<String>,
74}
75
76/// Error returned when maximum retry attempts have been exhausted.
77///
78/// This error is raised when a request continues to fail with 429 or 500
79/// responses after all configured retry attempts have been made.
80///
81/// # Example
82///
83/// ```rust
84/// use shopify_sdk::clients::MaxHttpRetriesExceededError;
85///
86/// let error = MaxHttpRetriesExceededError {
87/// code: 429,
88/// tries: 3,
89/// message: r#"{"error":"Rate limited"}"#.to_string(),
90/// error_reference: None,
91/// };
92///
93/// println!("{}", error); // "Exceeded maximum retry count of 3. Last message: ..."
94/// ```
95#[derive(Debug, Error)]
96#[error("Exceeded maximum retry count of {tries}. Last message: {message}")]
97pub struct MaxHttpRetriesExceededError {
98 /// The HTTP status code of the last response.
99 pub code: u16,
100 /// The number of tries that were attempted.
101 pub tries: u32,
102 /// Serialized error message from the last response.
103 pub message: String,
104 /// Reference ID for error reporting (from X-Request-Id header).
105 pub error_reference: Option<String>,
106}
107
108/// Error returned when an HTTP request fails validation.
109///
110/// This error is raised before a request is sent if it fails validation
111/// checks, such as:
112/// - Missing body for POST/PUT requests
113/// - Body provided without `body_type`
114/// - Invalid HTTP method
115///
116/// # Example
117///
118/// ```rust
119/// use shopify_sdk::clients::InvalidHttpRequestError;
120///
121/// let error = InvalidHttpRequestError::MissingBody {
122/// method: "post".to_string(),
123/// };
124///
125/// println!("{}", error); // "Cannot use post without specifying data."
126/// ```
127#[derive(Debug, Error, Clone, PartialEq, Eq)]
128pub enum InvalidHttpRequestError {
129 /// The HTTP method is not one of the supported methods.
130 #[error("Invalid Http method {method}.")]
131 InvalidMethod {
132 /// The invalid method that was provided.
133 method: String,
134 },
135
136 /// A request body was provided without specifying the body type.
137 #[error("Cannot set a body without also setting body_type.")]
138 MissingBodyType,
139
140 /// A POST or PUT request was made without a body.
141 #[error("Cannot use {method} without specifying data.")]
142 MissingBody {
143 /// The HTTP method that requires a body.
144 method: String,
145 },
146}
147
148/// Unified error type for all HTTP-related errors.
149///
150/// This enum provides a single error type for HTTP operations, making it
151/// easier to handle errors at API boundaries. Use pattern matching to
152/// handle specific error types.
153///
154/// # Example
155///
156/// ```rust,ignore
157/// use shopify_sdk::HttpError;
158///
159/// let result = client.request(request).await;
160/// match result {
161/// Ok(response) => { /* handle success */ }
162/// Err(HttpError::Response(e)) => { /* handle API error */ }
163/// Err(HttpError::MaxRetries(e)) => { /* handle retry exhaustion */ }
164/// Err(HttpError::InvalidRequest(e)) => { /* handle validation error */ }
165/// Err(HttpError::Network(e)) => { /* handle network error */ }
166/// }
167/// ```
168#[derive(Debug, Error)]
169pub enum HttpError {
170 /// An HTTP response error (non-2xx status code).
171 #[error(transparent)]
172 Response(#[from] HttpResponseError),
173
174 /// Maximum retry attempts exhausted.
175 #[error(transparent)]
176 MaxRetries(#[from] MaxHttpRetriesExceededError),
177
178 /// Request validation failed.
179 #[error(transparent)]
180 InvalidRequest(#[from] InvalidHttpRequestError),
181
182 /// Network or connection error.
183 #[error("Network error: {0}")]
184 Network(#[from] reqwest::Error),
185}
186
187#[cfg(test)]
188mod tests {
189 use super::*;
190
191 #[test]
192 fn test_http_response_error_includes_status_code_in_message() {
193 let error = HttpResponseError {
194 code: 404,
195 message: r#"{"error":"Not Found"}"#.to_string(),
196 error_reference: None,
197 };
198 // The error message should contain the message field
199 assert_eq!(error.to_string(), r#"{"error":"Not Found"}"#);
200 }
201
202 #[test]
203 fn test_http_response_error_includes_request_id() {
204 let error = HttpResponseError {
205 code: 500,
206 message: r#"{"error":"Internal Server Error","error_reference":"If you report this error, please include this id: abc-123."}"#.to_string(),
207 error_reference: Some("abc-123".to_string()),
208 };
209 assert_eq!(error.error_reference, Some("abc-123".to_string()));
210 assert!(error.to_string().contains("abc-123"));
211 }
212
213 #[test]
214 fn test_max_retries_error_includes_retry_count() {
215 let error = MaxHttpRetriesExceededError {
216 code: 429,
217 tries: 3,
218 message: r#"{"error":"Rate limited"}"#.to_string(),
219 error_reference: None,
220 };
221 let message = error.to_string();
222 assert!(message.contains("3"));
223 assert!(message.contains("Exceeded maximum retry count"));
224 }
225
226 #[test]
227 fn test_invalid_request_error_missing_body() {
228 let error = InvalidHttpRequestError::MissingBody {
229 method: "post".to_string(),
230 };
231 assert_eq!(
232 error.to_string(),
233 "Cannot use post without specifying data."
234 );
235 }
236
237 #[test]
238 fn test_invalid_request_error_missing_body_type() {
239 let error = InvalidHttpRequestError::MissingBodyType;
240 assert_eq!(
241 error.to_string(),
242 "Cannot set a body without also setting body_type."
243 );
244 }
245
246 #[test]
247 fn test_error_types_implement_std_error() {
248 let http_error: &dyn std::error::Error = &HttpResponseError {
249 code: 400,
250 message: "test".to_string(),
251 error_reference: None,
252 };
253 let _ = http_error;
254
255 let max_retries_error: &dyn std::error::Error = &MaxHttpRetriesExceededError {
256 code: 429,
257 tries: 3,
258 message: "test".to_string(),
259 error_reference: None,
260 };
261 let _ = max_retries_error;
262
263 let invalid_error: &dyn std::error::Error = &InvalidHttpRequestError::MissingBodyType;
264 let _ = invalid_error;
265 }
266}