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}