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
62impl ApiError {
63    /// `true` for HTTP 400 Bad Request, typically because the request failed validation.
64    ///
65    /// The reference clients throw a distinct `InvalidRequestError` subclass for this status;
66    /// since Rust has no exception hierarchy to mirror, these `is_*` predicates are the
67    /// idiomatic equivalent classification on the one [`ApiError`] type.
68    pub fn is_invalid_request(&self) -> bool {
69        self.status_code == 400
70    }
71
72    /// `true` for HTTP 401 Unauthorized: the token is missing or invalid.
73    pub fn is_unauthorized(&self) -> bool {
74        self.status_code == 401
75    }
76
77    /// `true` for HTTP 403 Forbidden: the token lacks permission for the operation.
78    pub fn is_forbidden(&self) -> bool {
79        self.status_code == 403
80    }
81
82    /// `true` for HTTP 404 Not Found.
83    ///
84    /// Most `get`-style methods already map a `404` to `None` rather than an error (see e.g.
85    /// [`crate::clients::actor::ActorClient::get`]), so this is mainly useful for the methods
86    /// that return other errors directly, e.g. [`crate::clients::actor::ActorClient::start`]
87    /// with a nonexistent Actor ID.
88    pub fn is_not_found(&self) -> bool {
89        self.status_code == 404
90    }
91
92    /// `true` for HTTP 409 Conflict.
93    pub fn is_conflict(&self) -> bool {
94        self.status_code == 409
95    }
96
97    /// `true` for HTTP 429 Too Many Requests. The client already retries these internally (see
98    /// [`crate::ApifyClientBuilder::max_retries`]), so this surfaces only once retries are
99    /// exhausted.
100    pub fn is_rate_limited(&self) -> bool {
101        self.status_code == 429
102    }
103
104    /// `true` for an HTTP 5xx status. Like [`is_rate_limited`](Self::is_rate_limited), the
105    /// client already retries these internally, so this surfaces only once retries are
106    /// exhausted.
107    pub fn is_server_error(&self) -> bool {
108        self.status_code >= 500
109    }
110}
111
112/// The top-level error type for all client operations: every method returns
113/// `Result<T, ApifyClientError>`, so one `match`/`if let` already catches every kind of failure.
114#[derive(Debug, thiserror::Error)]
115pub enum ApifyClientError {
116    /// The API returned a non-success status code with a structured error body.
117    ///
118    /// Boxed to keep the overall `Result` size small (the error path is rare).
119    #[error(transparent)]
120    Api(Box<ApiError>),
121
122    /// A network/transport-level error occurred (connection failure, timeout, etc.).
123    #[error("HTTP transport error: {0}")]
124    Http(String),
125
126    /// The request timed out.
127    #[error("Request timed out")]
128    Timeout,
129
130    /// Failed to serialize the request body or deserialize the response body.
131    #[error("(De)serialization error: {0}")]
132    Serde(#[from] serde_json::Error),
133
134    /// The response body could not be interpreted as expected.
135    #[error("Invalid response: {0}")]
136    InvalidResponse(String),
137
138    /// A required configuration value or argument was missing or invalid.
139    #[error("Invalid argument: {0}")]
140    InvalidArgument(String),
141}
142
143impl From<ApiError> for ApifyClientError {
144    fn from(err: ApiError) -> Self {
145        ApifyClientError::Api(Box::new(err))
146    }
147}
148
149impl ApifyClientError {
150    /// Returns the underlying [`ApiError`] if this is an API error, otherwise `None`.
151    pub fn as_api_error(&self) -> Option<&ApiError> {
152        match self {
153            ApifyClientError::Api(e) => Some(e),
154            _ => None,
155        }
156    }
157
158    /// Returns the HTTP status code if this error originated from an API response.
159    pub fn status_code(&self) -> Option<u16> {
160        self.as_api_error().map(|e| e.status_code)
161    }
162}
163
164impl From<reqwest::Error> for ApifyClientError {
165    fn from(err: reqwest::Error) -> Self {
166        if err.is_timeout() {
167            ApifyClientError::Timeout
168        } else {
169            ApifyClientError::Http(err.to_string())
170        }
171    }
172}