meilisearch_sdk/
errors.rs

1use serde::{Deserialize, Serialize};
2use thiserror::Error;
3
4/// An enum representing the errors that can occur.
5
6#[derive(Debug, Error)]
7#[non_exhaustive]
8pub enum Error {
9    /// The exhaustive list of Meilisearch errors: <https://github.com/meilisearch/specifications/blob/main/text/0061-error-format-and-definitions.md>
10    ///
11    /// Also check out: <https://github.com/meilisearch/meilisearch/blob/main/crates/meilisearch-types/src/error.rs>
12    #[error(transparent)]
13    Meilisearch(#[from] MeilisearchError),
14
15    #[error(transparent)]
16    MeilisearchCommunication(#[from] MeilisearchCommunicationError),
17    /// The Meilisearch server returned an invalid JSON for a request.
18    #[error("Error parsing response JSON: {}", .0)]
19    ParseError(#[from] serde_json::Error),
20
21    /// A timeout happened while waiting for an update to complete.
22    #[error("A task did not succeed in time.")]
23    Timeout,
24    /// This Meilisearch SDK generated an invalid request (which was not sent).
25    ///
26    /// It probably comes from an invalid API key resulting in an invalid HTTP header.
27    #[error("Unable to generate a valid HTTP request. It probably comes from an invalid API key.")]
28    InvalidRequest,
29
30    /// Can't call this method without setting an api key in the client.
31    #[error("You need to provide an api key to use the `{0}` method.")]
32    CantUseWithoutApiKey(String),
33    /// It is not possible to generate a tenant token with an invalid api key.
34    ///
35    /// Empty strings or with less than 8 characters are considered invalid.
36    #[error("The provided api_key is invalid.")]
37    TenantTokensInvalidApiKey,
38    /// It is not possible to generate an already expired tenant token.
39    #[error("The provided expires_at is already expired.")]
40    TenantTokensExpiredSignature,
41
42    /// When jsonwebtoken cannot generate the token successfully.
43    #[cfg(not(target_arch = "wasm32"))]
44    #[error("Impossible to generate the token, jsonwebtoken encountered an error: {}", .0)]
45    InvalidTenantToken(#[from] jsonwebtoken::errors::Error),
46
47    /// The http client encountered an error.
48    #[cfg(feature = "reqwest")]
49    #[error("HTTP request failed: {}", .0)]
50    HttpError(#[from] reqwest::Error),
51
52    /// The library formatting the query parameters encountered an error.
53    #[error("Internal Error: could not parse the query parameters: {}", .0)]
54    Yaup(#[from] yaup::Error),
55
56    /// The library validating the format of an uuid.
57    #[cfg(not(target_arch = "wasm32"))]
58    #[error("The uid of the token has bit an uuid4 format: {}", .0)]
59    Uuid(#[from] uuid::Error),
60
61    /// Error thrown in case the version of the Uuid is not v4.
62    #[error("The uid provided to the token is not of version uuidv4")]
63    InvalidUuid4Version,
64
65    #[error(transparent)]
66    Other(Box<dyn std::error::Error + Send + Sync + 'static>),
67}
68
69#[derive(Debug, Clone, Deserialize, Error)]
70#[serde(rename_all = "camelCase")]
71pub struct MeilisearchCommunicationError {
72    pub status_code: u16,
73    pub message: Option<String>,
74    pub url: String,
75}
76
77impl std::fmt::Display for MeilisearchCommunicationError {
78    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79        write!(
80            f,
81            "MeilisearchCommunicationError: The server responded with a {}.",
82            self.status_code
83        )?;
84        if let Some(message) = &self.message {
85            write!(f, " {message}")?;
86        }
87        write!(f, "\nurl: {}", self.url)?;
88        Ok(())
89    }
90}
91
92#[derive(Debug, Clone, Deserialize, Error)]
93#[serde(rename_all = "camelCase")]
94#[error("Meilisearch {}: {}: {}. {}", .error_type, .error_code, .error_message, .error_link)]
95pub struct MeilisearchError {
96    /// The human readable error message
97    #[serde(rename = "message")]
98    pub error_message: String,
99    /// The error code of the error.  Officially documented at
100    /// <https://www.meilisearch.com/docs/reference/errors/error_codes>.
101    #[serde(rename = "code")]
102    pub error_code: ErrorCode,
103    /// The type of error (invalid request, internal error, or authentication error)
104    #[serde(rename = "type")]
105    pub error_type: ErrorType,
106    /// A link to the Meilisearch documentation for an error.
107    #[serde(rename = "link")]
108    pub error_link: String,
109}
110
111/// The type of error that was encountered.
112#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
113#[serde(rename_all = "snake_case")]
114#[non_exhaustive]
115pub enum ErrorType {
116    /// The submitted request was invalid.
117    InvalidRequest,
118    /// The Meilisearch instance encountered an internal error.
119    Internal,
120    /// Authentication was either incorrect or missing.
121    Auth,
122
123    /// That's unexpected. Please open a GitHub issue after ensuring you are
124    /// using the supported version of the Meilisearch server.
125    #[serde(other)]
126    Unknown,
127}
128
129impl std::fmt::Display for ErrorType {
130    fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
131        write!(
132            fmt,
133            "{}",
134            // this can't fail
135            serde_json::to_value(self).unwrap().as_str().unwrap()
136        )
137    }
138}
139
140/// The error code.
141///
142/// Officially documented at <https://www.meilisearch.com/docs/reference/errors/error_codes>.
143#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
144#[serde(rename_all = "snake_case")]
145#[non_exhaustive]
146pub enum ErrorCode {
147    IndexCreationFailed,
148    IndexAlreadyExists,
149    IndexNotFound,
150    InvalidIndexUid,
151    InvalidState,
152    PrimaryKeyInferenceFailed,
153    IndexPrimaryKeyAlreadyPresent,
154    InvalidStoreFile,
155    MaxFieldsLimitExceeded,
156    MissingDocumentId,
157    InvalidDocumentId,
158    BadParameter,
159    BadRequest,
160    DatabaseSizeLimitReached,
161    DocumentNotFound,
162    InternalError,
163    InvalidApiKey,
164    MissingAuthorizationHeader,
165    TaskNotFound,
166    DumpNotFound,
167    MissingMasterKey,
168    NoSpaceLeftOnDevice,
169    PayloadTooLarge,
170    UnretrievableDocument,
171    SearchError,
172    UnsupportedMediaType,
173    DumpAlreadyProcessing,
174    DumpProcessFailed,
175    MissingContentType,
176    MalformedPayload,
177    InvalidContentType,
178    MissingPayload,
179    InvalidApiKeyDescription,
180    InvalidApiKeyActions,
181    InvalidApiKeyIndexes,
182    InvalidApiKeyExpiresAt,
183    ApiKeyNotFound,
184    MissingTaskFilters,
185    MissingIndexUid,
186    InvalidIndexOffset,
187    InvalidIndexLimit,
188    InvalidIndexPrimaryKey,
189    InvalidDocumentFilter,
190    MissingDocumentFilter,
191    InvalidDocumentFields,
192    InvalidDocumentLimit,
193    InvalidDocumentOffset,
194    InvalidDocumentGeoField,
195    InvalidSearchQ,
196    InvalidSearchOffset,
197    InvalidSearchLimit,
198    InvalidSearchPage,
199    InvalidSearchHitsPerPage,
200    InvalidSearchAttributesToRetrieve,
201    InvalidSearchAttributesToCrop,
202    InvalidSearchCropLength,
203    InvalidSearchAttributesToHighlight,
204    InvalidSearchShowMatchesPosition,
205    InvalidSearchFilter,
206    InvalidSearchSort,
207    InvalidSearchFacets,
208    InvalidSearchHighlightPreTag,
209    InvalidSearchHighlightPostTag,
210    InvalidSearchCropMarker,
211    InvalidSearchMatchingStrategy,
212    ImmutableApiKeyUid,
213    ImmutableApiKeyActions,
214    ImmutableApiKeyIndexes,
215    ImmutableExpiresAt,
216    ImmutableCreatedAt,
217    ImmutableUpdatedAt,
218    InvalidSwapDuplicateIndexFound,
219    InvalidSwapIndexes,
220    MissingSwapIndexes,
221    InvalidTaskTypes,
222    InvalidTaskUids,
223    InvalidTaskStatuses,
224    InvalidTaskLimit,
225    InvalidTaskFrom,
226    InvalidTaskCanceledBy,
227    InvalidTaskFilters,
228    TooManyOpenFiles,
229    IoError,
230    InvalidTaskIndexUids,
231    ImmutableIndexUid,
232    ImmutableIndexCreatedAt,
233    ImmutableIndexUpdatedAt,
234    InvalidSettingsDisplayedAttributes,
235    InvalidSettingsSearchableAttributes,
236    InvalidSettingsFilterableAttributes,
237    InvalidSettingsSortableAttributes,
238    InvalidSettingsRankingRules,
239    InvalidSettingsStopWords,
240    InvalidSettingsSynonyms,
241    InvalidSettingsDistinctAttributes,
242    InvalidSettingsTypoTolerance,
243    InvalidSettingsFaceting,
244    InvalidSettingsDictionary,
245    InvalidSettingsPagination,
246    InvalidTaskBeforeEnqueuedAt,
247    InvalidTaskAfterEnqueuedAt,
248    InvalidTaskBeforeStartedAt,
249    InvalidTaskAfterStartedAt,
250    InvalidTaskBeforeFinishedAt,
251    InvalidTaskAfterFinishedAt,
252    MissingApiKeyActions,
253    MissingApiKeyIndexes,
254    MissingApiKeyExpiresAt,
255    InvalidApiKeyLimit,
256    InvalidApiKeyOffset,
257
258    /// That's unexpected. Please open a GitHub issue after ensuring you are
259    /// using the supported version of the Meilisearch server.
260    #[serde(other)]
261    Unknown,
262}
263
264pub const MEILISEARCH_VERSION_HINT: &str = "Hint: It might not be working because you're not up to date with the Meilisearch version that updated the get_documents_with method";
265
266impl std::fmt::Display for ErrorCode {
267    fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
268        write!(
269            fmt,
270            "{}",
271            // this can't fail
272            serde_json::to_value(self).unwrap().as_str().unwrap()
273        )
274    }
275}
276
277#[cfg(test)]
278mod test {
279    use super::*;
280
281    use jsonwebtoken::errors::ErrorKind::InvalidToken;
282    use meilisearch_test_macro::meilisearch_test;
283    use uuid::Uuid;
284
285    #[meilisearch_test]
286    async fn test_meilisearch_error() {
287        let error: MeilisearchError = serde_json::from_str(
288            r#"
289{
290  "message": "The cool error message.",
291  "code": "index_creation_failed",
292  "type": "internal",
293  "link": "https://the best link ever"
294}"#,
295        )
296        .unwrap();
297
298        assert_eq!(error.error_message, "The cool error message.");
299        assert_eq!(error.error_code, ErrorCode::IndexCreationFailed);
300        assert_eq!(error.error_type, ErrorType::Internal);
301        assert_eq!(error.error_link, "https://the best link ever");
302
303        let error: MeilisearchError = serde_json::from_str(
304            r#"
305{
306  "message": "",
307  "code": "An unknown error",
308  "type": "An unknown type",
309  "link": ""
310}"#,
311        )
312        .unwrap();
313
314        assert_eq!(error.error_code, ErrorCode::Unknown);
315        assert_eq!(error.error_type, ErrorType::Unknown);
316    }
317
318    #[meilisearch_test]
319    async fn test_error_message_parsing() {
320        let error: MeilisearchError = serde_json::from_str(
321            r#"
322{
323  "message": "The cool error message.",
324  "code": "index_creation_failed",
325  "type": "internal",
326  "link": "https://the best link ever"
327}"#,
328        )
329        .unwrap();
330
331        assert_eq!(error.to_string(), "Meilisearch internal: index_creation_failed: The cool error message.. https://the best link ever");
332
333        let error: MeilisearchCommunicationError = MeilisearchCommunicationError {
334            status_code: 404,
335            message: Some("Hint: something.".to_string()),
336            url: "http://localhost:7700/something".to_string(),
337        };
338
339        assert_eq!(
340            error.to_string(),
341            "MeilisearchCommunicationError: The server responded with a 404. Hint: something.\nurl: http://localhost:7700/something"
342        );
343
344        let error: MeilisearchCommunicationError = MeilisearchCommunicationError {
345            status_code: 404,
346            message: None,
347            url: "http://localhost:7700/something".to_string(),
348        };
349
350        assert_eq!(
351            error.to_string(),
352            "MeilisearchCommunicationError: The server responded with a 404.\nurl: http://localhost:7700/something"
353        );
354
355        let error = Error::Timeout;
356        assert_eq!(error.to_string(), "A task did not succeed in time.");
357
358        let error = Error::InvalidRequest;
359        assert_eq!(
360            error.to_string(),
361            "Unable to generate a valid HTTP request. It probably comes from an invalid API key."
362        );
363
364        let error = Error::TenantTokensInvalidApiKey;
365        assert_eq!(error.to_string(), "The provided api_key is invalid.");
366
367        let error = Error::TenantTokensExpiredSignature;
368        assert_eq!(
369            error.to_string(),
370            "The provided expires_at is already expired."
371        );
372
373        let error = Error::InvalidUuid4Version;
374        assert_eq!(
375            error.to_string(),
376            "The uid provided to the token is not of version uuidv4"
377        );
378
379        let error = Error::Uuid(Uuid::parse_str("67e55044").unwrap_err());
380        assert_eq!(error.to_string(), "The uid of the token has bit an uuid4 format: invalid length: expected length 32 for simple format, found 8");
381
382        let data = r#"
383        {
384            "name": "John Doe"
385            "age": 43,
386        }"#;
387
388        let error = Error::ParseError(serde_json::from_str::<String>(data).unwrap_err());
389        assert_eq!(
390            error.to_string(),
391            "Error parsing response JSON: invalid type: map, expected a string at line 2 column 8"
392        );
393
394        let error = Error::HttpError(
395            reqwest::Client::new()
396                .execute(reqwest::Request::new(
397                    reqwest::Method::POST,
398                    // there will never be a `meilisearch.gouv.fr` addr since these domain name are controlled by the state of france
399                    reqwest::Url::parse("https://meilisearch.gouv.fr").unwrap(),
400                ))
401                .await
402                .unwrap_err(),
403        );
404        assert_eq!(
405            error.to_string(),
406            "HTTP request failed: error sending request for url (https://meilisearch.gouv.fr/)"
407        );
408
409        let error = Error::InvalidTenantToken(jsonwebtoken::errors::Error::from(InvalidToken));
410        assert_eq!(
411            error.to_string(),
412            "Impossible to generate the token, jsonwebtoken encountered an error: InvalidToken"
413        );
414
415        let error = Error::Yaup(yaup::Error::Custom("Test yaup error".to_string()));
416        assert_eq!(
417            error.to_string(),
418            "Internal Error: could not parse the query parameters: Test yaup error"
419        );
420    }
421}