clawspec_core/client/
error.rs

1use std::fmt::Debug;
2
3use super::auth::AuthenticationError;
4use super::response::output::Output;
5
6/// Errors that can occur when using the ApiClient.
7///
8/// This enum covers all possible error conditions from network issues to data validation failures.
9/// All variants implement `std::error::Error` and provide detailed context for debugging.
10#[derive(Debug, derive_more::Error, derive_more::Display, derive_more::From)]
11pub enum ApiClientError {
12    /// HTTP client error from the underlying reqwest library.
13    ///
14    /// Occurs when network requests fail, timeouts occur, or connection issues arise.
15    ReqwestError(reqwest::Error),
16
17    /// URL parsing error when constructing request URLs.
18    ///
19    /// Occurs when the base URL or path parameters create an invalid URL.
20    UrlError(url::ParseError),
21
22    /// HTTP header processing error.
23    ///
24    /// Occurs when header values cannot be processed or are malformed.
25    HeadersError(headers::Error),
26
27    /// HTTP protocol error from the http crate.
28    ///
29    /// Occurs when HTTP protocol constraints are violated.
30    HttpError(http::Error),
31
32    /// Invalid HTTP header name.
33    ///
34    /// Occurs when attempting to create headers with invalid names.
35    InvalidHeaderName(http::header::InvalidHeaderName),
36
37    /// Invalid HTTP header value.
38    ///
39    /// Occurs when header values contain invalid characters.
40    InvalidHeaderValue(http::header::InvalidHeaderValue),
41
42    /// JSON serialization/deserialization error.
43    ///
44    /// Occurs when working with JSON request bodies or responses.
45    JsonValueError(serde_json::Error),
46
47    /// Query parameter serialization error.
48    ///
49    /// Occurs when converting structures to URL query strings.
50    QuerySerializationError(serde_urlencoded::ser::Error),
51
52    /// Authentication processing error.
53    ///
54    /// Occurs when authentication credentials cannot be processed or are invalid.
55    AuthenticationError(AuthenticationError),
56
57    /// No call result available for operation.
58    ///
59    /// Occurs when attempting to access response data before making a request.
60    #[display("Invalid state: expected a call result")]
61    CallResultRequired,
62
63    /// Invalid base path configuration.
64    ///
65    /// Occurs when the provided base path cannot be used for URL construction.
66    #[display("Invalid base path: {error}")]
67    InvalidBasePath {
68        /// Description of why the base path is invalid.
69        error: String,
70    },
71
72    /// JSON response deserialization failure.
73    ///
74    /// Occurs when the response body cannot be parsed as the expected JSON structure.
75    #[display("Failed to deserialize JSON at '{path}': {error}\n{body}")]
76    #[from(skip)]
77    JsonError {
78        /// The request path where the error occurred.
79        path: String,
80        /// The underlying JSON parsing error.
81        error: serde_json::Error,
82        /// The response body that failed to parse.
83        body: String,
84    },
85
86    /// Response output type is incompatible with JSON deserialization.
87    ///
88    /// Occurs when attempting to parse non-JSON responses as JSON.
89    #[display("Unsupported output for {name} as JSON:\n{output:?}")]
90    #[from(skip)]
91    UnsupportedJsonOutput {
92        /// The actual response output received.
93        output: Output,
94        /// Name of the operation that failed.
95        name: &'static str,
96    },
97
98    /// Response output type is incompatible with text extraction.
99    ///
100    /// Occurs when attempting to extract text from binary or empty responses.
101    #[display("Unsupported output for text:\n{output:?}")]
102    #[from(skip)]
103    UnsupportedTextOutput {
104        /// The actual response output received.
105        output: Output,
106    },
107
108    /// Response output type is incompatible with byte extraction.
109    ///
110    /// Occurs when attempting to extract bytes from empty responses.
111    #[display("Unsupported output for bytes:\n{output:?}")]
112    #[from(skip)]
113    UnsupportedBytesOutput {
114        /// The actual response output received.
115        output: Output,
116    },
117
118    /// Path template contains unresolved parameters.
119    ///
120    /// Occurs when path parameters are missing for templated URLs.
121    #[display("Path '{path}' is missing required arguments: {missings:?}")]
122    #[from(skip)]
123    PathUnresolved {
124        /// The path template that couldn't be resolved.
125        path: String,
126        /// List of missing parameter names.
127        missings: Vec<String>,
128    },
129
130    /// Query parameter value type is not supported.
131    ///
132    /// Occurs when attempting to use complex objects as query parameters.
133    #[display(
134        "Unsupported query parameter value: objects are not supported for query parameters. Got: {value}"
135    )]
136    #[from(skip)]
137    UnsupportedQueryParameterValue {
138        /// The unsupported value that was provided.
139        value: serde_json::Value,
140    },
141
142    /// Parameter value cannot be converted to the required format.
143    ///
144    /// Occurs when parameter values are incompatible with their target type.
145    #[display("Unsupported parameter value: {message}. Got: {value}")]
146    #[from(skip)]
147    UnsupportedParameterValue {
148        /// Specific error message describing the conversion failure.
149        message: String,
150        /// The value that failed to convert.
151        value: serde_json::Value,
152    },
153
154    /// OpenAPI operation not found in the specification.
155    ///
156    /// Occurs when referencing operations that don't exist in the collected spec.
157    #[display("Missing operation: {id}")]
158    #[from(skip)]
159    MissingOperation {
160        /// The operation ID that was not found.
161        id: String,
162    },
163
164    /// Server returned an internal error (HTTP 500).
165    ///
166    /// Occurs when the server encounters an internal error during request processing.
167    #[display("Server error (500) with response body: {raw_body}")]
168    #[from(skip)]
169    ServerFailure {
170        /// The response body containing error details.
171        raw_body: String,
172    },
173
174    /// Data serialization failed.
175    ///
176    /// Occurs when request data cannot be converted to the required format.
177    #[display("Serialization error: {message}")]
178    #[from(skip)]
179    SerializationError {
180        /// Description of the serialization failure.
181        message: String,
182    },
183
184    /// Server returned an unexpected HTTP status code.
185    ///
186    /// Occurs when the response status code doesn't match expected values.
187    #[display("Unexpected status code {status_code}: {body}")]
188    #[from(skip)]
189    UnexpectedStatusCode {
190        /// The unexpected HTTP status code received.
191        status_code: u16,
192        /// The response body for debugging.
193        body: String,
194    },
195
196    /// JSON redaction operation failed.
197    ///
198    /// Occurs when applying redactions to JSON responses.
199    #[cfg(feature = "redaction")]
200    #[display("Redaction error: {message}")]
201    #[from(skip)]
202    RedactionError {
203        /// Description of the redaction failure.
204        message: String,
205    },
206
207    /// Response output type doesn't match expected type.
208    ///
209    /// Occurs when attempting operations on incompatible response types.
210    #[display("Expected output type '{expected}' but got '{actual}'")]
211    #[from(skip)]
212    UnexpectedOutputType {
213        /// The expected output type.
214        expected: String,
215        /// The actual output type received.
216        actual: String,
217    },
218}
219
220#[cfg(test)]
221mod tests {
222    use super::*;
223    use crate::client::response::output::Output;
224
225    #[test]
226    fn test_api_client_error_is_send_and_sync() {
227        fn assert_send<T: Send>() {}
228        fn assert_sync<T: Sync>() {}
229
230        assert_send::<ApiClientError>();
231        assert_sync::<ApiClientError>();
232    }
233
234    // Test custom error variants (those with #[from(skip)])
235    #[test]
236    fn test_call_result_required_error() {
237        let error = ApiClientError::CallResultRequired;
238        assert_eq!(error.to_string(), "Invalid state: expected a call result");
239    }
240
241    #[test]
242    fn test_invalid_base_path_error() {
243        let error = ApiClientError::InvalidBasePath {
244            error: "contains invalid characters".to_string(),
245        };
246        assert_eq!(
247            error.to_string(),
248            "Invalid base path: contains invalid characters"
249        );
250    }
251
252    #[test]
253    fn test_json_error() {
254        // Create a real JSON error by trying to parse invalid JSON
255        let json_error = serde_json::from_str::<serde_json::Value>("{ invalid json").unwrap_err();
256        let error = ApiClientError::JsonError {
257            path: "/api/users".to_string(),
258            error: json_error,
259            body: "{ invalid json }".to_string(),
260        };
261
262        let error_str = error.to_string();
263        assert!(error_str.contains("Failed to deserialize JSON at '/api/users'"));
264        assert!(error_str.contains("{ invalid json }"));
265    }
266
267    #[test]
268    fn test_unsupported_json_output_error() {
269        let output = Output::Bytes(vec![0xFF, 0xFE, 0xFD]);
270        let error = ApiClientError::UnsupportedJsonOutput {
271            output,
272            name: "User",
273        };
274
275        let error_str = error.to_string();
276        assert!(error_str.contains("Unsupported output for User as JSON"));
277        assert!(error_str.contains("Bytes"));
278    }
279
280    #[test]
281    fn test_unsupported_text_output_error() {
282        let output = Output::Bytes(vec![0xFF, 0xFE, 0xFD]);
283        let error = ApiClientError::UnsupportedTextOutput { output };
284
285        let error_str = error.to_string();
286        assert!(error_str.contains("Unsupported output for text"));
287        assert!(error_str.contains("Bytes"));
288    }
289
290    #[test]
291    fn test_unsupported_bytes_output_error() {
292        let output = Output::Empty;
293        let error = ApiClientError::UnsupportedBytesOutput { output };
294
295        let error_str = error.to_string();
296        assert!(error_str.contains("Unsupported output for bytes"));
297        assert!(error_str.contains("Empty"));
298    }
299
300    #[test]
301    fn test_path_unresolved_error() {
302        let error = ApiClientError::PathUnresolved {
303            path: "/users/{id}/posts/{post_id}".to_string(),
304            missings: vec!["id".to_string(), "post_id".to_string()],
305        };
306
307        let error_str = error.to_string();
308        assert!(
309            error_str.contains("Path '/users/{id}/posts/{post_id}' is missing required arguments")
310        );
311        assert!(error_str.contains("id"));
312        assert!(error_str.contains("post_id"));
313    }
314
315    #[test]
316    fn test_unsupported_query_parameter_value_error() {
317        let value = serde_json::json!({"nested": {"object": "not supported"}});
318        let error = ApiClientError::UnsupportedQueryParameterValue {
319            value: value.clone(),
320        };
321
322        let error_str = error.to_string();
323        assert!(error_str.contains("Unsupported query parameter value"));
324        assert!(error_str.contains("objects are not supported"));
325    }
326
327    #[test]
328    fn test_unsupported_parameter_value_error() {
329        let value = serde_json::json!({"complex": "object"});
330        let error = ApiClientError::UnsupportedParameterValue {
331            message: "nested objects not allowed".to_string(),
332            value: value.clone(),
333        };
334
335        let error_str = error.to_string();
336        assert!(error_str.contains("Unsupported parameter value: nested objects not allowed"));
337        assert!(error_str.contains("complex"));
338    }
339
340    #[test]
341    fn test_missing_operation_error() {
342        let error = ApiClientError::MissingOperation {
343            id: "get-users-by-id".to_string(),
344        };
345
346        assert_eq!(error.to_string(), "Missing operation: get-users-by-id");
347    }
348
349    #[test]
350    fn test_server_failure_error() {
351        let error = ApiClientError::ServerFailure {
352            raw_body: "Internal Server Error: Database connection failed".to_string(),
353        };
354
355        let error_str = error.to_string();
356        assert!(error_str.contains("Server error (500)"));
357        assert!(error_str.contains("Database connection failed"));
358    }
359
360    #[test]
361    fn test_serialization_error() {
362        let error = ApiClientError::SerializationError {
363            message: "Cannot serialize circular reference".to_string(),
364        };
365
366        assert_eq!(
367            error.to_string(),
368            "Serialization error: Cannot serialize circular reference"
369        );
370    }
371
372    #[test]
373    fn test_unexpected_status_code_error() {
374        let error = ApiClientError::UnexpectedStatusCode {
375            status_code: 418,
376            body: "I'm a teapot".to_string(),
377        };
378
379        let error_str = error.to_string();
380        assert!(error_str.contains("Unexpected status code 418"));
381        assert!(error_str.contains("I'm a teapot"));
382    }
383
384    // Test automatic conversions from underlying error types
385    #[test]
386    fn test_from_reqwest_error() {
387        // Create a simple URL parse error and convert it to reqwest error
388        let url_error = url::ParseError::InvalidPort;
389        let api_error: ApiClientError = url_error.into();
390
391        match api_error {
392            ApiClientError::UrlError(_) => {} // Expected - testing URL error conversion
393            _ => panic!("Should convert to UrlError"),
394        }
395    }
396
397    #[test]
398    fn test_from_url_parse_error() {
399        let url_error = url::ParseError::InvalidPort;
400        let api_error: ApiClientError = url_error.into();
401
402        match api_error {
403            ApiClientError::UrlError(url::ParseError::InvalidPort) => {} // Expected
404            _ => panic!("Should convert to UrlError"),
405        }
406    }
407
408    #[test]
409    fn test_from_json_error() {
410        let json_error = serde_json::from_str::<serde_json::Value>("invalid").unwrap_err();
411        let api_error: ApiClientError = json_error.into();
412
413        match api_error {
414            ApiClientError::JsonValueError(_) => {} // Expected
415            _ => panic!("Should convert to JsonValueError"),
416        }
417    }
418
419    #[test]
420    fn test_from_http_error() {
421        // Create an HTTP error by trying to parse an invalid header name
422        let invalid_header = http::HeaderName::from_bytes(b"invalid\0header").unwrap_err();
423        let api_error: ApiClientError = invalid_header.into();
424
425        match api_error {
426            ApiClientError::InvalidHeaderName(_) => {} // Expected - testing InvalidHeaderName conversion
427            _ => panic!("Should convert to InvalidHeaderName"),
428        }
429    }
430
431    #[test]
432    fn test_from_invalid_header_name() {
433        let header_error = http::HeaderName::from_bytes(b"invalid\0header").unwrap_err();
434        let api_error: ApiClientError = header_error.into();
435
436        match api_error {
437            ApiClientError::InvalidHeaderName(_) => {} // Expected
438            _ => panic!("Should convert to InvalidHeaderName"),
439        }
440    }
441
442    #[test]
443    fn test_from_invalid_header_value() {
444        // Header values cannot contain control characters (0x00-0x1F except tab)
445        let header_error = http::HeaderValue::from_bytes(&[0x00]).unwrap_err();
446        let api_error: ApiClientError = header_error.into();
447
448        match api_error {
449            ApiClientError::InvalidHeaderValue(_) => {} // Expected
450            _ => panic!("Should convert to InvalidHeaderValue"),
451        }
452    }
453
454    #[test]
455    fn test_from_authentication_error() {
456        let auth_error = AuthenticationError::InvalidBearerToken {
457            message: "contains null byte".to_string(),
458        };
459        let api_error: ApiClientError = auth_error.into();
460
461        match api_error {
462            ApiClientError::AuthenticationError(_) => {} // Expected
463            _ => panic!("Should convert to AuthenticationError"),
464        }
465    }
466
467    // Test Debug implementation
468    #[test]
469    fn test_error_debug_implementation() {
470        let error = ApiClientError::CallResultRequired;
471        let debug_str = format!("{error:?}");
472        assert!(debug_str.contains("CallResultRequired"));
473
474        let error = ApiClientError::InvalidBasePath {
475            error: "test".to_string(),
476        };
477        let debug_str = format!("{error:?}");
478        assert!(debug_str.contains("InvalidBasePath"));
479        assert!(debug_str.contains("test"));
480    }
481
482    // Test that errors implement the Error trait properly
483    #[test]
484    fn test_error_trait_implementation() {
485        use std::error::Error;
486
487        let error = ApiClientError::CallResultRequired;
488        assert!(error.source().is_none());
489
490        let json_error = serde_json::from_str::<serde_json::Value>("invalid").unwrap_err();
491        let error = ApiClientError::JsonValueError(json_error);
492        assert!(error.source().is_some());
493    }
494
495    // Test error equality (where applicable)
496    #[test]
497    fn test_error_equality() {
498        let error1 = ApiClientError::CallResultRequired;
499        let error2 = ApiClientError::CallResultRequired;
500
501        // These errors should produce the same string representation
502        assert_eq!(error1.to_string(), error2.to_string());
503
504        let error1 = ApiClientError::InvalidBasePath {
505            error: "same error".to_string(),
506        };
507        let error2 = ApiClientError::InvalidBasePath {
508            error: "same error".to_string(),
509        };
510        assert_eq!(error1.to_string(), error2.to_string());
511    }
512
513    // Test error context preservation
514    #[test]
515    fn test_error_context_preservation() {
516        let path = "/complex/path/{id}";
517        let missings = vec!["id".to_string(), "user_id".to_string()];
518        let error = ApiClientError::PathUnresolved {
519            path: path.to_string(),
520            missings: missings.clone(),
521        };
522
523        let error_string = error.to_string();
524        assert!(error_string.contains(path));
525        for missing in &missings {
526            assert!(error_string.contains(missing));
527        }
528    }
529
530    // Test complex error scenarios
531    #[test]
532    fn test_json_error_with_large_body() {
533        let large_body = "x".repeat(2000);
534        let json_error = serde_json::from_str::<serde_json::Value>("{ invalid").unwrap_err();
535        let error = ApiClientError::JsonError {
536            path: "/api/data".to_string(),
537            error: json_error,
538            body: large_body.clone(),
539        };
540
541        let error_str = error.to_string();
542        assert!(error_str.contains("/api/data"));
543        assert!(error_str.contains(&large_body));
544    }
545
546    #[test]
547    fn test_status_code_error_edge_cases() {
548        // Test various HTTP status codes
549        let error = ApiClientError::UnexpectedStatusCode {
550            status_code: 999, // Invalid status code
551            body: "unknown status".to_string(),
552        };
553        assert!(error.to_string().contains("999"));
554
555        let error = ApiClientError::UnexpectedStatusCode {
556            status_code: 0, // Edge case
557            body: "".to_string(),
558        };
559        assert!(error.to_string().contains("0"));
560    }
561
562    // Test output error variants with different Output types
563    #[test]
564    fn test_output_errors_with_all_output_types() {
565        // Test with Text output
566        let text_output = Output::Text("some text".to_string());
567        let error = ApiClientError::UnsupportedBytesOutput {
568            output: text_output,
569        };
570        assert!(error.to_string().contains("Text"));
571
572        // Test with JSON output
573        let json_output =
574            Output::Json(serde_json::to_string(&serde_json::json!({"key": "value"})).unwrap());
575        let error = ApiClientError::UnsupportedTextOutput {
576            output: json_output,
577        };
578        assert!(error.to_string().contains("Json"));
579
580        // Test with Empty output
581        let empty_output = Output::Empty;
582        let error = ApiClientError::UnsupportedJsonOutput {
583            output: empty_output,
584            name: "TestType",
585        };
586        assert!(error.to_string().contains("Empty"));
587        assert!(error.to_string().contains("TestType"));
588    }
589}