pinecone_sdk/utils/
errors.rs

1use crate::openapi::apis::{Error as OpenApiError, ResponseContent};
2use anyhow::Error as AnyhowError;
3use reqwest::{self, StatusCode};
4use thiserror::Error;
5
6/// PineconeError is the error type for all Pinecone SDK errors.
7#[derive(Error, Debug)]
8pub enum PineconeError {
9    /// UnknownResponseError: Unknown response error.
10    #[error("Unknown response error: status: {status}, message: {message}")]
11    UnknownResponseError {
12        /// status code
13        status: StatusCode,
14        /// message
15        message: String,
16    },
17
18    /// ActionForbiddenError: Action is forbidden.
19    #[error("Action forbidden error: {source}")]
20    ActionForbiddenError {
21        /// Source error
22        source: WrappedResponseContent,
23    },
24
25    /// APIKeyMissingError: API key is not provided as an argument nor in the environment variable `PINECONE_API_KEY`.
26    #[error("API key missing error: {message}")]
27    APIKeyMissingError {
28        /// Error message.
29        message: String,
30    },
31
32    /// InvalidHeadersError: Provided headers are not valid. Expects JSON.
33    #[error("Invalid headers error: {message}")]
34    InvalidHeadersError {
35        /// Error message.
36        message: String,
37    },
38
39    /// TimeoutError: Request timed out.
40    #[error("Timeout error: {message}")]
41    TimeoutError {
42        /// Error message.
43        message: String,
44    },
45
46    /// ConnectionError: Failed to establish a connection.
47    #[error("Connection error: {source}")]
48    ConnectionError {
49        /// Source of the error.
50        source: AnyhowError,
51    },
52
53    /// ReqwestError: Error caused by Reqwest
54    #[error("Reqwest error: {source}")]
55    ReqwestError {
56        /// Source of the error.
57        source: AnyhowError,
58    },
59
60    /// SerdeError: Error caused by Serde
61    #[error("Serde error: {source}")]
62    SerdeError {
63        /// Source of the error.
64        source: AnyhowError,
65    },
66
67    /// IoError: Error caused by IO
68    #[error("IO error: {message}")]
69    IoError {
70        /// Error message.
71        message: String,
72    },
73
74    /// BadRequestError: Bad request. The request body included invalid request parameters
75    #[error("Bad request error: {source}")]
76    BadRequestError {
77        /// Source error
78        source: WrappedResponseContent,
79    },
80
81    /// UnauthorizedError: Unauthorized. Possibly caused by invalid API key
82    #[error("Unauthorized error: {source}")]
83    UnauthorizedError {
84        /// Source error
85        source: WrappedResponseContent,
86    },
87
88    /// PodQuotaExceededError: Pod quota exceeded
89    #[error("Pod quota exceeded error: {source}")]
90    PodQuotaExceededError {
91        /// Source error
92        source: WrappedResponseContent,
93    },
94
95    /// CollectionsQuotaExceededError: Collections quota exceeded
96    #[error("Collections quota exceeded error: {source}")]
97    CollectionsQuotaExceededError {
98        /// Source error
99        source: WrappedResponseContent,
100    },
101
102    /// InvalidCloudError: Provided cloud is not valid.
103    #[error("Invalid cloud error: {source}")]
104    InvalidCloudError {
105        /// Source error
106        source: WrappedResponseContent,
107    },
108
109    /// InvalidRegionError: Provided region is not valid.
110    #[error("Invalid region error: {source}")]
111    InvalidRegionError {
112        /// Source error
113        source: WrappedResponseContent,
114    },
115
116    /// InvalidConfigurationError: Provided configuration is not valid.
117    #[error("Invalid configuration error: {message}")]
118    InvalidConfigurationError {
119        /// Error message.
120        message: String,
121    },
122
123    /// CollectionNotFoundError: Collection of given name does not exist
124    #[error("Collection not found error: {source}")]
125    CollectionNotFoundError {
126        /// Source error
127        source: WrappedResponseContent,
128    },
129
130    /// IndexNotFoundError: Index of given name does not exist
131    #[error("Index not found error: {source}")]
132    IndexNotFoundError {
133        /// Source error
134        source: WrappedResponseContent,
135    },
136
137    /// ResourceAlreadyExistsError: Resource of given name already exists
138    #[error("Resource already exists error: {source}")]
139    ResourceAlreadyExistsError {
140        /// Source error
141        source: WrappedResponseContent,
142    },
143
144    /// Unprocessable entity error: The request body could not be deserialized
145    #[error("Unprocessable entity error: {source}")]
146    UnprocessableEntityError {
147        /// Source error
148        source: WrappedResponseContent,
149    },
150
151    /// PendingCollectionError: There is a pending collection created from this index
152    #[error("Pending collection error: {source}")]
153    PendingCollectionError {
154        /// Source error
155        source: WrappedResponseContent,
156    },
157
158    /// InternalServerError: Internal server error
159    #[error("Internal server error: {source}")]
160    InternalServerError {
161        /// Source error
162        source: WrappedResponseContent,
163    },
164
165    /// DataPlaneError: Failed to perform a data plane operation.
166    #[error("Data plane error: {status}")]
167    DataPlaneError {
168        /// Error status
169        status: tonic::Status,
170    },
171
172    /// InferenceError: Failed to perform an inference operation.
173    #[error("Inference error: {status}")]
174    InferenceError {
175        /// Error status
176        status: tonic::Status,
177    },
178}
179
180// Implement the conversion from OpenApiError to PineconeError for CreateIndexError.
181impl<T> From<OpenApiError<T>> for PineconeError {
182    fn from(error: OpenApiError<T>) -> Self {
183        match error {
184            OpenApiError::Reqwest(inner) => PineconeError::ReqwestError {
185                source: inner.into(),
186            },
187            OpenApiError::Serde(inner) => PineconeError::SerdeError {
188                source: inner.into(),
189            },
190            OpenApiError::Io(inner) => PineconeError::IoError {
191                message: inner.to_string(),
192            },
193            OpenApiError::ResponseError(inner) => handle_response_error(inner.into()),
194        }
195    }
196}
197
198// Helper function to handle response errors
199fn handle_response_error(source: WrappedResponseContent) -> PineconeError {
200    let status = source.status;
201    let message = source.content.clone();
202
203    match status {
204        StatusCode::BAD_REQUEST => PineconeError::BadRequestError { source },
205        StatusCode::UNAUTHORIZED => PineconeError::UnauthorizedError { source },
206        StatusCode::FORBIDDEN => parse_forbidden_error(source, message),
207        StatusCode::NOT_FOUND => parse_not_found_error(source, message),
208        StatusCode::CONFLICT => PineconeError::ResourceAlreadyExistsError { source },
209        StatusCode::PRECONDITION_FAILED => PineconeError::PendingCollectionError { source },
210        StatusCode::UNPROCESSABLE_ENTITY => PineconeError::UnprocessableEntityError { source },
211        StatusCode::INTERNAL_SERVER_ERROR => PineconeError::InternalServerError { source },
212        _ => PineconeError::UnknownResponseError { status, message },
213    }
214}
215
216fn parse_not_found_error(source: WrappedResponseContent, message: String) -> PineconeError {
217    if message.contains("Index") {
218        PineconeError::IndexNotFoundError { source }
219    } else if message.contains("Collection") {
220        PineconeError::CollectionNotFoundError { source }
221    } else if message.contains("region") {
222        PineconeError::InvalidRegionError { source }
223    } else if message.contains("cloud") {
224        PineconeError::InvalidCloudError { source }
225    } else {
226        PineconeError::InternalServerError { source }
227    }
228}
229
230fn parse_forbidden_error(source: WrappedResponseContent, message: String) -> PineconeError {
231    if message.contains("Deletion protection") {
232        PineconeError::ActionForbiddenError { source }
233    } else if message.contains("index") {
234        PineconeError::PodQuotaExceededError { source }
235    } else if message.contains("Collection") {
236        PineconeError::CollectionsQuotaExceededError { source }
237    } else {
238        PineconeError::InternalServerError { source }
239    }
240}
241
242/// WrappedResponseContent is a wrapper around ResponseContent.
243#[derive(Debug)]
244pub struct WrappedResponseContent {
245    /// status code
246    pub status: reqwest::StatusCode,
247    /// content
248    pub content: String,
249}
250
251impl<T> From<ResponseContent<T>> for WrappedResponseContent {
252    fn from(rc: ResponseContent<T>) -> Self {
253        WrappedResponseContent {
254            status: rc.status,
255            content: rc.content,
256        }
257    }
258}
259
260impl std::error::Error for WrappedResponseContent {
261    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
262        None
263    }
264}
265
266impl std::fmt::Display for WrappedResponseContent {
267    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
268        write!(f, "status: {} content: {}", self.status, self.content)
269    }
270}
271
272#[cfg(test)]
273mod tests {
274    use super::PineconeError;
275    use tokio;
276
277    fn assert_send_sync<T: Send + Sync>() {}
278
279    #[tokio::test]
280    async fn test_pinecone_error_is_send_sync() {
281        assert_send_sync::<PineconeError>();
282    }
283}