Skip to main content

apify_client/
error.rs

1//! Error types returned by the Apify client.
2
3use serde::Deserialize;
4
5/// The result type used throughout this crate.
6pub type ApifyClientResult<T> = Result<T, ApifyClientError>;
7
8/// Shape of the `error` object returned by the Apify API on failure.
9///
10/// The API encodes errors as `{ "error": { "type": "...", "message": "..." } }`.
11#[derive(Debug, Clone, Deserialize)]
12pub(crate) struct ApiErrorBody {
13    pub error: ApiErrorDetail,
14}
15
16#[derive(Debug, Clone, Deserialize)]
17pub(crate) struct ApiErrorDetail {
18    #[serde(rename = "type")]
19    pub error_type: Option<String>,
20    pub message: Option<String>,
21    pub data: Option<serde_json::Value>,
22}
23
24/// An error response returned by the Apify API.
25///
26/// This is raised for HTTP requests that reach the API but return a non-success
27/// status code. It mirrors the `ApifyApiError` of the reference clients and exposes
28/// the parsed error `type`, the human-readable `message`, the HTTP `status_code`,
29/// the number of the final `attempt`, and the request `http_method`/`path`.
30#[derive(Debug, Clone)]
31pub struct ApiError {
32    /// HTTP status code of the error response.
33    pub status_code: u16,
34    /// The machine-readable error type returned by the API (e.g. `record-not-found`).
35    pub error_type: Option<String>,
36    /// Human-readable description of the error returned by the API.
37    pub message: String,
38    /// Number of the API call attempt that produced this error (1-based).
39    pub attempt: u32,
40    /// HTTP method of the API call (e.g. `GET`, `POST`).
41    pub http_method: Option<String>,
42    /// Full path of the API endpoint (URL excluding origin).
43    pub path: Option<String>,
44    /// Additional structured data provided by the API about the error, if any.
45    pub data: Option<serde_json::Value>,
46}
47
48impl std::fmt::Display for ApiError {
49    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50        write!(
51            f,
52            "Apify API error (status {}, type {}): {}",
53            self.status_code,
54            self.error_type.as_deref().unwrap_or("unknown"),
55            self.message,
56        )
57    }
58}
59
60impl std::error::Error for ApiError {}
61
62/// The top-level error type for all client operations.
63#[derive(Debug, thiserror::Error)]
64pub enum ApifyClientError {
65    /// The API returned a non-success status code with a structured error body.
66    ///
67    /// Boxed to keep the overall `Result` size small (the error path is rare).
68    #[error(transparent)]
69    Api(Box<ApiError>),
70
71    /// A network/transport-level error occurred (connection failure, timeout, etc.).
72    #[error("HTTP transport error: {0}")]
73    Http(String),
74
75    /// The request timed out.
76    #[error("Request timed out")]
77    Timeout,
78
79    /// Failed to serialize the request body or deserialize the response body.
80    #[error("(De)serialization error: {0}")]
81    Serde(#[from] serde_json::Error),
82
83    /// The response body could not be interpreted as expected.
84    #[error("Invalid response: {0}")]
85    InvalidResponse(String),
86
87    /// A required configuration value or argument was missing or invalid.
88    #[error("Invalid argument: {0}")]
89    InvalidArgument(String),
90}
91
92impl From<ApiError> for ApifyClientError {
93    fn from(err: ApiError) -> Self {
94        ApifyClientError::Api(Box::new(err))
95    }
96}
97
98impl ApifyClientError {
99    /// Returns the underlying [`ApiError`] if this is an API error, otherwise `None`.
100    pub fn as_api_error(&self) -> Option<&ApiError> {
101        match self {
102            ApifyClientError::Api(e) => Some(e),
103            _ => None,
104        }
105    }
106
107    /// Returns the HTTP status code if this error originated from an API response.
108    pub fn status_code(&self) -> Option<u16> {
109        self.as_api_error().map(|e| e.status_code)
110    }
111}
112
113impl From<reqwest::Error> for ApifyClientError {
114    fn from(err: reqwest::Error) -> Self {
115        if err.is_timeout() {
116            ApifyClientError::Timeout
117        } else {
118            ApifyClientError::Http(err.to_string())
119        }
120    }
121}