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    /// OAuth2 authentication error.
220    ///
221    /// Occurs when OAuth2 token acquisition or refresh fails.
222    #[cfg(feature = "oauth2")]
223    #[display("OAuth2 error: {message}")]
224    #[from(skip)]
225    OAuth2Error {
226        /// Description of the OAuth2 error.
227        message: String,
228    },
229}
230
231#[cfg(feature = "oauth2")]
232impl ApiClientError {
233    /// Creates an OAuth2 error from any error type that implements Display.
234    pub fn oauth2_error(error: impl std::fmt::Display) -> Self {
235        Self::OAuth2Error {
236            message: error.to_string(),
237        }
238    }
239}
240
241#[cfg(test)]
242mod tests {
243    use super::*;
244    use crate::client::response::output::Output;
245
246    #[test]
247    fn test_api_client_error_is_send_and_sync() {
248        fn assert_send<T: Send>() {}
249        fn assert_sync<T: Sync>() {}
250
251        assert_send::<ApiClientError>();
252        assert_sync::<ApiClientError>();
253    }
254
255    // Test custom error variants (those with #[from(skip)])
256    #[test]
257    fn test_call_result_required_error() {
258        let error = ApiClientError::CallResultRequired;
259        assert_eq!(error.to_string(), "Invalid state: expected a call result");
260    }
261
262    #[test]
263    fn test_invalid_base_path_error() {
264        let error = ApiClientError::InvalidBasePath {
265            error: "contains invalid characters".to_string(),
266        };
267        assert_eq!(
268            error.to_string(),
269            "Invalid base path: contains invalid characters"
270        );
271    }
272
273    #[test]
274    fn test_json_error() {
275        // Create a real JSON error by trying to parse invalid JSON
276        let json_error = serde_json::from_str::<serde_json::Value>("{ invalid json").unwrap_err();
277        let error = ApiClientError::JsonError {
278            path: "/api/users".to_string(),
279            error: json_error,
280            body: "{ invalid json }".to_string(),
281        };
282
283        let error_str = error.to_string();
284        assert!(error_str.contains("Failed to deserialize JSON at '/api/users'"));
285        assert!(error_str.contains("{ invalid json }"));
286    }
287
288    #[test]
289    fn test_unsupported_json_output_error() {
290        let output = Output::Bytes(vec![0xFF, 0xFE, 0xFD]);
291        let error = ApiClientError::UnsupportedJsonOutput {
292            output,
293            name: "User",
294        };
295
296        let error_str = error.to_string();
297        assert!(error_str.contains("Unsupported output for User as JSON"));
298        assert!(error_str.contains("Bytes"));
299    }
300
301    #[test]
302    fn test_unsupported_text_output_error() {
303        let output = Output::Bytes(vec![0xFF, 0xFE, 0xFD]);
304        let error = ApiClientError::UnsupportedTextOutput { output };
305
306        let error_str = error.to_string();
307        assert!(error_str.contains("Unsupported output for text"));
308        assert!(error_str.contains("Bytes"));
309    }
310
311    #[test]
312    fn test_unsupported_bytes_output_error() {
313        let output = Output::Empty;
314        let error = ApiClientError::UnsupportedBytesOutput { output };
315
316        let error_str = error.to_string();
317        assert!(error_str.contains("Unsupported output for bytes"));
318        assert!(error_str.contains("Empty"));
319    }
320
321    #[test]
322    fn test_path_unresolved_error() {
323        let error = ApiClientError::PathUnresolved {
324            path: "/users/{id}/posts/{post_id}".to_string(),
325            missings: vec!["id".to_string(), "post_id".to_string()],
326        };
327
328        let error_str = error.to_string();
329        assert!(
330            error_str.contains("Path '/users/{id}/posts/{post_id}' is missing required arguments")
331        );
332        assert!(error_str.contains("id"));
333        assert!(error_str.contains("post_id"));
334    }
335
336    #[test]
337    fn test_unsupported_query_parameter_value_error() {
338        let value = serde_json::json!({"nested": {"object": "not supported"}});
339        let error = ApiClientError::UnsupportedQueryParameterValue {
340            value: value.clone(),
341        };
342
343        let error_str = error.to_string();
344        assert!(error_str.contains("Unsupported query parameter value"));
345        assert!(error_str.contains("objects are not supported"));
346    }
347
348    #[test]
349    fn test_unsupported_parameter_value_error() {
350        let value = serde_json::json!({"complex": "object"});
351        let error = ApiClientError::UnsupportedParameterValue {
352            message: "nested objects not allowed".to_string(),
353            value: value.clone(),
354        };
355
356        let error_str = error.to_string();
357        assert!(error_str.contains("Unsupported parameter value: nested objects not allowed"));
358        assert!(error_str.contains("complex"));
359    }
360
361    #[test]
362    fn test_missing_operation_error() {
363        let error = ApiClientError::MissingOperation {
364            id: "get-users-by-id".to_string(),
365        };
366
367        assert_eq!(error.to_string(), "Missing operation: get-users-by-id");
368    }
369
370    #[test]
371    fn test_server_failure_error() {
372        let error = ApiClientError::ServerFailure {
373            raw_body: "Internal Server Error: Database connection failed".to_string(),
374        };
375
376        let error_str = error.to_string();
377        assert!(error_str.contains("Server error (500)"));
378        assert!(error_str.contains("Database connection failed"));
379    }
380
381    #[test]
382    fn test_serialization_error() {
383        let error = ApiClientError::SerializationError {
384            message: "Cannot serialize circular reference".to_string(),
385        };
386
387        assert_eq!(
388            error.to_string(),
389            "Serialization error: Cannot serialize circular reference"
390        );
391    }
392
393    #[test]
394    fn test_unexpected_status_code_error() {
395        let error = ApiClientError::UnexpectedStatusCode {
396            status_code: 418,
397            body: "I'm a teapot".to_string(),
398        };
399
400        let error_str = error.to_string();
401        assert!(error_str.contains("Unexpected status code 418"));
402        assert!(error_str.contains("I'm a teapot"));
403    }
404
405    // Test automatic conversions from underlying error types
406    #[test]
407    fn test_from_reqwest_error() {
408        // Create a simple URL parse error and convert it to reqwest error
409        let url_error = url::ParseError::InvalidPort;
410        let api_error: ApiClientError = url_error.into();
411
412        match api_error {
413            ApiClientError::UrlError(_) => {} // Expected - testing URL error conversion
414            _ => panic!("Should convert to UrlError"),
415        }
416    }
417
418    #[test]
419    fn test_from_url_parse_error() {
420        let url_error = url::ParseError::InvalidPort;
421        let api_error: ApiClientError = url_error.into();
422
423        match api_error {
424            ApiClientError::UrlError(url::ParseError::InvalidPort) => {} // Expected
425            _ => panic!("Should convert to UrlError"),
426        }
427    }
428
429    #[test]
430    fn test_from_json_error() {
431        let json_error = serde_json::from_str::<serde_json::Value>("invalid").unwrap_err();
432        let api_error: ApiClientError = json_error.into();
433
434        match api_error {
435            ApiClientError::JsonValueError(_) => {} // Expected
436            _ => panic!("Should convert to JsonValueError"),
437        }
438    }
439
440    #[test]
441    fn test_from_http_error() {
442        // Create an HTTP error by trying to parse an invalid header name
443        let invalid_header = http::HeaderName::from_bytes(b"invalid\0header").unwrap_err();
444        let api_error: ApiClientError = invalid_header.into();
445
446        match api_error {
447            ApiClientError::InvalidHeaderName(_) => {} // Expected - testing InvalidHeaderName conversion
448            _ => panic!("Should convert to InvalidHeaderName"),
449        }
450    }
451
452    #[test]
453    fn test_from_invalid_header_name() {
454        let header_error = http::HeaderName::from_bytes(b"invalid\0header").unwrap_err();
455        let api_error: ApiClientError = header_error.into();
456
457        match api_error {
458            ApiClientError::InvalidHeaderName(_) => {} // Expected
459            _ => panic!("Should convert to InvalidHeaderName"),
460        }
461    }
462
463    #[test]
464    fn test_from_invalid_header_value() {
465        // Header values cannot contain control characters (0x00-0x1F except tab)
466        let header_error = http::HeaderValue::from_bytes(&[0x00]).unwrap_err();
467        let api_error: ApiClientError = header_error.into();
468
469        match api_error {
470            ApiClientError::InvalidHeaderValue(_) => {} // Expected
471            _ => panic!("Should convert to InvalidHeaderValue"),
472        }
473    }
474
475    #[test]
476    fn test_from_authentication_error() {
477        let auth_error = AuthenticationError::InvalidBearerToken {
478            message: "contains null byte".to_string(),
479        };
480        let api_error: ApiClientError = auth_error.into();
481
482        match api_error {
483            ApiClientError::AuthenticationError(_) => {} // Expected
484            _ => panic!("Should convert to AuthenticationError"),
485        }
486    }
487
488    // Test Debug implementation
489    #[test]
490    fn test_error_debug_implementation() {
491        let error = ApiClientError::CallResultRequired;
492        let debug_str = format!("{error:?}");
493        assert!(debug_str.contains("CallResultRequired"));
494
495        let error = ApiClientError::InvalidBasePath {
496            error: "test".to_string(),
497        };
498        let debug_str = format!("{error:?}");
499        assert!(debug_str.contains("InvalidBasePath"));
500        assert!(debug_str.contains("test"));
501    }
502
503    // Test that errors implement the Error trait properly
504    #[test]
505    fn test_error_trait_implementation() {
506        use std::error::Error;
507
508        let error = ApiClientError::CallResultRequired;
509        assert!(error.source().is_none());
510
511        let json_error = serde_json::from_str::<serde_json::Value>("invalid").unwrap_err();
512        let error = ApiClientError::JsonValueError(json_error);
513        assert!(error.source().is_some());
514    }
515
516    // Test error equality (where applicable)
517    #[test]
518    fn test_error_equality() {
519        let error1 = ApiClientError::CallResultRequired;
520        let error2 = ApiClientError::CallResultRequired;
521
522        // These errors should produce the same string representation
523        assert_eq!(error1.to_string(), error2.to_string());
524
525        let error1 = ApiClientError::InvalidBasePath {
526            error: "same error".to_string(),
527        };
528        let error2 = ApiClientError::InvalidBasePath {
529            error: "same error".to_string(),
530        };
531        assert_eq!(error1.to_string(), error2.to_string());
532    }
533
534    // Test error context preservation
535    #[test]
536    fn test_error_context_preservation() {
537        let path = "/complex/path/{id}";
538        let missings = vec!["id".to_string(), "user_id".to_string()];
539        let error = ApiClientError::PathUnresolved {
540            path: path.to_string(),
541            missings: missings.clone(),
542        };
543
544        let error_string = error.to_string();
545        assert!(error_string.contains(path));
546        for missing in &missings {
547            assert!(error_string.contains(missing));
548        }
549    }
550
551    // Test complex error scenarios
552    #[test]
553    fn test_json_error_with_large_body() {
554        let large_body = "x".repeat(2000);
555        let json_error = serde_json::from_str::<serde_json::Value>("{ invalid").unwrap_err();
556        let error = ApiClientError::JsonError {
557            path: "/api/data".to_string(),
558            error: json_error,
559            body: large_body.clone(),
560        };
561
562        let error_str = error.to_string();
563        assert!(error_str.contains("/api/data"));
564        assert!(error_str.contains(&large_body));
565    }
566
567    #[test]
568    fn test_status_code_error_edge_cases() {
569        // Test various HTTP status codes
570        let error = ApiClientError::UnexpectedStatusCode {
571            status_code: 999, // Invalid status code
572            body: "unknown status".to_string(),
573        };
574        assert!(error.to_string().contains("999"));
575
576        let error = ApiClientError::UnexpectedStatusCode {
577            status_code: 0, // Edge case
578            body: "".to_string(),
579        };
580        assert!(error.to_string().contains("0"));
581    }
582
583    // Test output error variants with different Output types
584    #[test]
585    fn test_output_errors_with_all_output_types() {
586        // Test with Text output
587        let text_output = Output::Text("some text".to_string());
588        let error = ApiClientError::UnsupportedBytesOutput {
589            output: text_output,
590        };
591        assert!(error.to_string().contains("Text"));
592
593        // Test with JSON output
594        let json_output =
595            Output::Json(serde_json::to_string(&serde_json::json!({"key": "value"})).unwrap());
596        let error = ApiClientError::UnsupportedTextOutput {
597            output: json_output,
598        };
599        assert!(error.to_string().contains("Json"));
600
601        // Test with Empty output
602        let empty_output = Output::Empty;
603        let error = ApiClientError::UnsupportedJsonOutput {
604            output: empty_output,
605            name: "TestType",
606        };
607        assert!(error.to_string().contains("Empty"));
608        assert!(error.to_string().contains("TestType"));
609    }
610}