Skip to main content

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}