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.
113#[derive(Debug, thiserror::Error)]
114pub enum ApifyClientError {
115 /// The API returned a non-success status code with a structured error body.
116 ///
117 /// Boxed to keep the overall `Result` size small (the error path is rare).
118 #[error(transparent)]
119 Api(Box<ApiError>),
120
121 /// A network/transport-level error occurred (connection failure, timeout, etc.).
122 #[error("HTTP transport error: {0}")]
123 Http(String),
124
125 /// The request timed out.
126 #[error("Request timed out")]
127 Timeout,
128
129 /// Failed to serialize the request body or deserialize the response body.
130 #[error("(De)serialization error: {0}")]
131 Serde(#[from] serde_json::Error),
132
133 /// The response body could not be interpreted as expected.
134 #[error("Invalid response: {0}")]
135 InvalidResponse(String),
136
137 /// A required configuration value or argument was missing or invalid.
138 #[error("Invalid argument: {0}")]
139 InvalidArgument(String),
140}
141
142impl From<ApiError> for ApifyClientError {
143 fn from(err: ApiError) -> Self {
144 ApifyClientError::Api(Box::new(err))
145 }
146}
147
148impl ApifyClientError {
149 /// Returns the underlying [`ApiError`] if this is an API error, otherwise `None`.
150 pub fn as_api_error(&self) -> Option<&ApiError> {
151 match self {
152 ApifyClientError::Api(e) => Some(e),
153 _ => None,
154 }
155 }
156
157 /// Returns the HTTP status code if this error originated from an API response.
158 pub fn status_code(&self) -> Option<u16> {
159 self.as_api_error().map(|e| e.status_code)
160 }
161}
162
163impl From<reqwest::Error> for ApifyClientError {
164 fn from(err: reqwest::Error) -> Self {
165 if err.is_timeout() {
166 ApifyClientError::Timeout
167 } else {
168 ApifyClientError::Http(err.to_string())
169 }
170 }
171}