use std::{fmt, io}; use actix_web::http::StatusCode; use actix_web::{self as aweb, HttpResponseBuilder}; use aweb::rt::task::JoinError; use convert_case::Casing; use milli::heed::{Error as HeedError, MdbError}; use serde::{Deserialize, Serialize}; #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct ResponseError { #[serde(skip)] pub code: StatusCode, pub message: String, #[serde(rename = "code")] error_code: String, #[serde(rename = "type")] error_type: String, #[serde(rename = "link")] error_link: String, } impl ResponseError { pub fn from_msg(mut message: String, code: Code) -> Self { if code == Code::IoError { message.push_str(". This error generally happens when you have no space left on device or when your database doesn't have read or write right."); } Self { code: code.http(), message, error_code: code.name(), error_type: code.type_(), error_link: code.url(), } } } impl fmt::Display for ResponseError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.message.fmt(f) } } impl std::error::Error for ResponseError {} impl From for ResponseError where T: std::error::Error + ErrorCode, { fn from(other: T) -> Self { Self::from_msg(other.to_string(), other.error_code()) } } impl aweb::error::ResponseError for ResponseError { fn error_response(&self) -> aweb::HttpResponse { let json = serde_json::to_vec(self).unwrap(); HttpResponseBuilder::new(self.status_code()).content_type("application/json").body(json) } fn status_code(&self) -> StatusCode { self.code } } pub trait ErrorCode { fn error_code(&self) -> Code; /// returns the HTTP status code associated with the error fn http_status(&self) -> StatusCode { self.error_code().http() } /// returns the doc url associated with the error fn error_url(&self) -> String { self.error_code().url() } /// returns error name, used as error code fn error_name(&self) -> String { self.error_code().name() } /// return the error type fn error_type(&self) -> String { self.error_code().type_() } } #[allow(clippy::enum_variant_names)] enum ErrorType { Internal, InvalidRequest, Auth, System, } impl fmt::Display for ErrorType { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { use ErrorType::*; match self { Internal => write!(f, "internal"), InvalidRequest => write!(f, "invalid_request"), Auth => write!(f, "auth"), System => write!(f, "system"), } } } /// Implement all the error codes. /// /// 1. Make an enum `Code` where each error code is a variant /// 2. Implement the `http`, `name`, and `type_` method on the enum /// 3. Make a unit type for each error code in the module `deserr_codes`. /// /// The unit type's purpose is to be used as a marker type parameter, e.g. /// `DeserrJsonError`. It implements `Default` and `ErrorCode`, /// so we can get a value of the `Code` enum with the correct variant by calling /// `MyErrorCode::default().error_code()`. macro_rules! make_error_codes { ($($code_ident:ident, $err_type:ident, $status:ident);*) => { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Code { $($code_ident),* } impl Code { /// return the HTTP status code associated with the `Code` pub fn http(&self) -> StatusCode { match self { $( Code::$code_ident => StatusCode::$status ),* } } /// return error name, used as error code fn name(&self) -> String { match self { $( Code::$code_ident => stringify!($code_ident).to_case(convert_case::Case::Snake) ),* } } /// return the error type fn type_(&self) -> String { match self { $( Code::$code_ident => ErrorType::$err_type.to_string() ),* } } /// return the doc url associated with the error fn url(&self) -> String { format!("https://docs.meilisearch.com/errors#{}", self.name()) } } pub mod deserr_codes { use super::{Code, ErrorCode}; $( #[derive(Default)] pub struct $code_ident; impl ErrorCode for $code_ident { fn error_code(&self) -> Code { Code::$code_ident } } )* } } } // An exhaustive list of all the error codes used by meilisearch. make_error_codes! { ApiKeyAlreadyExists , InvalidRequest , CONFLICT ; ApiKeyNotFound , InvalidRequest , NOT_FOUND ; BadParameter , InvalidRequest , BAD_REQUEST; BadRequest , InvalidRequest , BAD_REQUEST; DatabaseSizeLimitReached , Internal , INTERNAL_SERVER_ERROR; DocumentNotFound , InvalidRequest , NOT_FOUND; DumpAlreadyProcessing , InvalidRequest , CONFLICT; DumpNotFound , InvalidRequest , NOT_FOUND; DumpProcessFailed , Internal , INTERNAL_SERVER_ERROR; DuplicateIndexFound , InvalidRequest , BAD_REQUEST; ImmutableApiKeyActions , InvalidRequest , BAD_REQUEST; ImmutableApiKeyCreatedAt , InvalidRequest , BAD_REQUEST; ImmutableApiKeyExpiresAt , InvalidRequest , BAD_REQUEST; ImmutableApiKeyIndexes , InvalidRequest , BAD_REQUEST; ImmutableApiKeyKey , InvalidRequest , BAD_REQUEST; ImmutableApiKeyUid , InvalidRequest , BAD_REQUEST; ImmutableApiKeyUpdatedAt , InvalidRequest , BAD_REQUEST; ImmutableIndexCreatedAt , InvalidRequest , BAD_REQUEST; ImmutableIndexUid , InvalidRequest , BAD_REQUEST; ImmutableIndexUpdatedAt , InvalidRequest , BAD_REQUEST; IndexAlreadyExists , InvalidRequest , CONFLICT ; IndexCreationFailed , Internal , INTERNAL_SERVER_ERROR; IndexNotFound , InvalidRequest , NOT_FOUND; IndexPrimaryKeyAlreadyExists , InvalidRequest , BAD_REQUEST ; IndexPrimaryKeyMultipleCandidatesFound, InvalidRequest , BAD_REQUEST; IndexPrimaryKeyNoCandidateFound , InvalidRequest , BAD_REQUEST ; Internal , Internal , INTERNAL_SERVER_ERROR ; InvalidApiKey , Auth , FORBIDDEN ; InvalidApiKeyActions , InvalidRequest , BAD_REQUEST ; InvalidApiKeyDescription , InvalidRequest , BAD_REQUEST ; InvalidApiKeyExpiresAt , InvalidRequest , BAD_REQUEST ; InvalidApiKeyIndexes , InvalidRequest , BAD_REQUEST ; InvalidApiKeyLimit , InvalidRequest , BAD_REQUEST ; InvalidApiKeyName , InvalidRequest , BAD_REQUEST ; InvalidApiKeyOffset , InvalidRequest , BAD_REQUEST ; InvalidApiKeyUid , InvalidRequest , BAD_REQUEST ; InvalidContentType , InvalidRequest , UNSUPPORTED_MEDIA_TYPE ; InvalidDocumentCsvDelimiter , InvalidRequest , BAD_REQUEST ; InvalidDocumentFields , InvalidRequest , BAD_REQUEST ; MissingDocumentFilter , InvalidRequest , BAD_REQUEST ; InvalidDocumentFilter , InvalidRequest , BAD_REQUEST ; InvalidDocumentGeoField , InvalidRequest , BAD_REQUEST ; InvalidVectorDimensions , InvalidRequest , BAD_REQUEST ; InvalidVectorsType , InvalidRequest , BAD_REQUEST ; InvalidDocumentId , InvalidRequest , BAD_REQUEST ; InvalidDocumentLimit , InvalidRequest , BAD_REQUEST ; InvalidDocumentOffset , InvalidRequest , BAD_REQUEST ; InvalidEmbedder , InvalidRequest , BAD_REQUEST ; InvalidHybridQuery , InvalidRequest , BAD_REQUEST ; InvalidIndexLimit , InvalidRequest , BAD_REQUEST ; InvalidIndexOffset , InvalidRequest , BAD_REQUEST ; InvalidIndexPrimaryKey , InvalidRequest , BAD_REQUEST ; InvalidIndexUid , InvalidRequest , BAD_REQUEST ; InvalidSearchAttributesToSearchOn , InvalidRequest , BAD_REQUEST ; InvalidSearchAttributesToCrop , InvalidRequest , BAD_REQUEST ; InvalidSearchAttributesToHighlight , InvalidRequest , BAD_REQUEST ; InvalidSearchAttributesToRetrieve , InvalidRequest , BAD_REQUEST ; InvalidSearchCropLength , InvalidRequest , BAD_REQUEST ; InvalidSearchCropMarker , InvalidRequest , BAD_REQUEST ; InvalidSearchFacets , InvalidRequest , BAD_REQUEST ; InvalidSearchSemanticRatio , InvalidRequest , BAD_REQUEST ; InvalidFacetSearchFacetName , InvalidRequest , BAD_REQUEST ; InvalidSearchFilter , InvalidRequest , BAD_REQUEST ; InvalidSearchHighlightPostTag , InvalidRequest , BAD_REQUEST ; InvalidSearchHighlightPreTag , InvalidRequest , BAD_REQUEST ; InvalidSearchHitsPerPage , InvalidRequest , BAD_REQUEST ; InvalidSearchLimit , InvalidRequest , BAD_REQUEST ; InvalidSearchMatchingStrategy , InvalidRequest , BAD_REQUEST ; InvalidSearchOffset , InvalidRequest , BAD_REQUEST ; InvalidSearchPage , InvalidRequest , BAD_REQUEST ; InvalidSearchQ , InvalidRequest , BAD_REQUEST ; InvalidFacetSearchQuery , InvalidRequest , BAD_REQUEST ; InvalidFacetSearchName , InvalidRequest , BAD_REQUEST ; InvalidSearchVector , InvalidRequest , BAD_REQUEST ; InvalidSearchShowMatchesPosition , InvalidRequest , BAD_REQUEST ; InvalidSearchShowRankingScore , InvalidRequest , BAD_REQUEST ; InvalidSearchShowRankingScoreDetails , InvalidRequest , BAD_REQUEST ; InvalidSearchSort , InvalidRequest , BAD_REQUEST ; InvalidSettingsDisplayedAttributes , InvalidRequest , BAD_REQUEST ; InvalidSettingsDistinctAttribute , InvalidRequest , BAD_REQUEST ; InvalidSettingsProximityPrecision , InvalidRequest , BAD_REQUEST ; InvalidSettingsFaceting , InvalidRequest , BAD_REQUEST ; InvalidSettingsFilterableAttributes , InvalidRequest , BAD_REQUEST ; InvalidSettingsPagination , InvalidRequest , BAD_REQUEST ; InvalidSettingsSearchCutoffMs , InvalidRequest , BAD_REQUEST ; InvalidSettingsEmbedders , InvalidRequest , BAD_REQUEST ; InvalidSettingsRankingRules , InvalidRequest , BAD_REQUEST ; InvalidSettingsSearchableAttributes , InvalidRequest , BAD_REQUEST ; InvalidSettingsSortableAttributes , InvalidRequest , BAD_REQUEST ; InvalidSettingsStopWords , InvalidRequest , BAD_REQUEST ; InvalidSettingsNonSeparatorTokens , InvalidRequest , BAD_REQUEST ; InvalidSettingsSeparatorTokens , InvalidRequest , BAD_REQUEST ; InvalidSettingsDictionary , InvalidRequest , BAD_REQUEST ; InvalidSettingsSynonyms , InvalidRequest , BAD_REQUEST ; InvalidSettingsTypoTolerance , InvalidRequest , BAD_REQUEST ; InvalidState , Internal , INTERNAL_SERVER_ERROR ; InvalidStoreFile , Internal , INTERNAL_SERVER_ERROR ; InvalidSwapDuplicateIndexFound , InvalidRequest , BAD_REQUEST ; InvalidSwapIndexes , InvalidRequest , BAD_REQUEST ; InvalidTaskAfterEnqueuedAt , InvalidRequest , BAD_REQUEST ; InvalidTaskAfterFinishedAt , InvalidRequest , BAD_REQUEST ; InvalidTaskAfterStartedAt , InvalidRequest , BAD_REQUEST ; InvalidTaskBeforeEnqueuedAt , InvalidRequest , BAD_REQUEST ; InvalidTaskBeforeFinishedAt , InvalidRequest , BAD_REQUEST ; InvalidTaskBeforeStartedAt , InvalidRequest , BAD_REQUEST ; InvalidTaskCanceledBy , InvalidRequest , BAD_REQUEST ; InvalidTaskFrom , InvalidRequest , BAD_REQUEST ; InvalidTaskLimit , InvalidRequest , BAD_REQUEST ; InvalidTaskStatuses , InvalidRequest , BAD_REQUEST ; InvalidTaskTypes , InvalidRequest , BAD_REQUEST ; InvalidTaskUids , InvalidRequest , BAD_REQUEST ; IoError , System , UNPROCESSABLE_ENTITY; FeatureNotEnabled , InvalidRequest , BAD_REQUEST ; MalformedPayload , InvalidRequest , BAD_REQUEST ; MaxFieldsLimitExceeded , InvalidRequest , BAD_REQUEST ; MissingApiKeyActions , InvalidRequest , BAD_REQUEST ; MissingApiKeyExpiresAt , InvalidRequest , BAD_REQUEST ; MissingApiKeyIndexes , InvalidRequest , BAD_REQUEST ; MissingAuthorizationHeader , Auth , UNAUTHORIZED ; MissingContentType , InvalidRequest , UNSUPPORTED_MEDIA_TYPE ; MissingDocumentId , InvalidRequest , BAD_REQUEST ; MissingFacetSearchFacetName , InvalidRequest , BAD_REQUEST ; MissingIndexUid , InvalidRequest , BAD_REQUEST ; MissingMasterKey , Auth , UNAUTHORIZED ; MissingPayload , InvalidRequest , BAD_REQUEST ; MissingSearchHybrid , InvalidRequest , BAD_REQUEST ; MissingSwapIndexes , InvalidRequest , BAD_REQUEST ; MissingTaskFilters , InvalidRequest , BAD_REQUEST ; NoSpaceLeftOnDevice , System , UNPROCESSABLE_ENTITY; PayloadTooLarge , InvalidRequest , PAYLOAD_TOO_LARGE ; TaskNotFound , InvalidRequest , NOT_FOUND ; TooManyOpenFiles , System , UNPROCESSABLE_ENTITY ; TooManyVectors , InvalidRequest , BAD_REQUEST ; UnretrievableDocument , Internal , BAD_REQUEST ; UnretrievableErrorCode , InvalidRequest , BAD_REQUEST ; UnsupportedMediaType , InvalidRequest , UNSUPPORTED_MEDIA_TYPE ; // Experimental features VectorEmbeddingError , InvalidRequest , BAD_REQUEST } impl ErrorCode for JoinError { fn error_code(&self) -> Code { Code::Internal } } impl ErrorCode for milli::Error { fn error_code(&self) -> Code { use milli::{Error, UserError}; match self { Error::InternalError(_) => Code::Internal, Error::IoError(e) => e.error_code(), Error::UserError(ref error) => { match error { // TODO: wait for spec for new error codes. UserError::SerdeJson(_) | UserError::InvalidLmdbOpenOptions | UserError::DocumentLimitReached | UserError::UnknownInternalDocumentId { .. } => Code::Internal, UserError::InvalidStoreFile => Code::InvalidStoreFile, UserError::NoSpaceLeftOnDevice => Code::NoSpaceLeftOnDevice, UserError::MaxDatabaseSizeReached => Code::DatabaseSizeLimitReached, UserError::AttributeLimitReached => Code::MaxFieldsLimitExceeded, UserError::InvalidFilter(_) => Code::InvalidSearchFilter, UserError::InvalidFilterExpression(..) => Code::InvalidSearchFilter, UserError::MissingDocumentId { .. } => Code::MissingDocumentId, UserError::InvalidDocumentId { .. } | UserError::TooManyDocumentIds { .. } => { Code::InvalidDocumentId } UserError::MissingDocumentField(_) => Code::InvalidDocumentFields, UserError::InvalidFieldForSource { .. } | UserError::MissingFieldForSource { .. } | UserError::InvalidOpenAiModel { .. } | UserError::InvalidOpenAiModelDimensions { .. } | UserError::InvalidOpenAiModelDimensionsMax { .. } | UserError::InvalidSettingsDimensions { .. } | UserError::InvalidUrl { .. } | UserError::InvalidPrompt(_) => Code::InvalidSettingsEmbedders, UserError::TooManyEmbedders(_) => Code::InvalidSettingsEmbedders, UserError::InvalidPromptForEmbeddings(..) => Code::InvalidSettingsEmbedders, UserError::NoPrimaryKeyCandidateFound => Code::IndexPrimaryKeyNoCandidateFound, UserError::MultiplePrimaryKeyCandidatesFound { .. } => { Code::IndexPrimaryKeyMultipleCandidatesFound } UserError::PrimaryKeyCannotBeChanged(_) => Code::IndexPrimaryKeyAlreadyExists, UserError::SortRankingRuleMissing => Code::InvalidSearchSort, UserError::InvalidFacetsDistribution { .. } => Code::InvalidSearchFacets, UserError::InvalidSortableAttribute { .. } => Code::InvalidSearchSort, UserError::InvalidSearchableAttribute { .. } => { Code::InvalidSearchAttributesToSearchOn } UserError::InvalidFacetSearchFacetName { .. } => { Code::InvalidFacetSearchFacetName } UserError::CriterionError(_) => Code::InvalidSettingsRankingRules, UserError::InvalidGeoField { .. } => Code::InvalidDocumentGeoField, UserError::InvalidVectorDimensions { .. } => Code::InvalidVectorDimensions, UserError::InvalidVectorsMapType { .. } => Code::InvalidVectorsType, UserError::InvalidVectorsType { .. } => Code::InvalidVectorsType, UserError::TooManyVectors(_, _) => Code::TooManyVectors, UserError::SortError(_) => Code::InvalidSearchSort, UserError::InvalidMinTypoWordLenSetting(_, _) => { Code::InvalidSettingsTypoTolerance } UserError::InvalidEmbedder(_) => Code::InvalidEmbedder, UserError::VectorEmbeddingError(_) => Code::VectorEmbeddingError, } } } } } impl ErrorCode for file_store::Error { fn error_code(&self) -> Code { match self { Self::IoError(e) => e.error_code(), Self::PersistError(e) => e.error_code(), Self::CouldNotParseFileNameAsUtf8 | Self::UuidError(_) => Code::Internal, } } } impl ErrorCode for tempfile::PersistError { fn error_code(&self) -> Code { self.error.error_code() } } impl ErrorCode for HeedError { fn error_code(&self) -> Code { match self { HeedError::Mdb(MdbError::MapFull) => Code::DatabaseSizeLimitReached, HeedError::Mdb(MdbError::Invalid) => Code::InvalidStoreFile, HeedError::Io(e) => e.error_code(), HeedError::Mdb(_) | HeedError::Encoding(_) | HeedError::Decoding(_) | HeedError::InvalidDatabaseTyping | HeedError::DatabaseClosing | HeedError::BadOpenOptions { .. } => Code::Internal, } } } impl ErrorCode for io::Error { fn error_code(&self) -> Code { match self.raw_os_error() { Some(5) => Code::IoError, Some(24) => Code::TooManyOpenFiles, Some(28) => Code::NoSpaceLeftOnDevice, _ => Code::Internal, } } } /// Deserialization when `deserr` cannot parse an API key date. #[derive(Debug)] pub struct ParseOffsetDateTimeError(pub String); impl fmt::Display for ParseOffsetDateTimeError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { writeln!(f, "`{original}` is not a valid date. It should follow the RFC 3339 format to represents a date or datetime in the future or specified as a null value. e.g. 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'.", original = self.0) } } /// Deserialization when `deserr` cannot parse a task date. #[derive(Debug)] pub struct InvalidTaskDateError(pub String); impl std::fmt::Display for InvalidTaskDateError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "`{}` is an invalid date-time. It should follow the YYYY-MM-DD or RFC 3339 date-time format.", self.0) } } /// Deserialization error when `deserr` cannot parse a String /// into a bool. #[derive(Debug)] pub struct DeserrParseBoolError(pub String); impl fmt::Display for DeserrParseBoolError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "could not parse `{}` as a boolean, expected either `true` or `false`", self.0) } } /// Deserialization error when `deserr` cannot parse a String /// into an integer. #[derive(Debug)] pub struct DeserrParseIntError(pub String); impl fmt::Display for DeserrParseIntError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "could not parse `{}` as a positive integer", self.0) } } impl fmt::Display for deserr_codes::InvalidSearchSemanticRatio { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, "the value of `semanticRatio` is invalid, expected a float between `0.0` and `1.0`." ) } } #[macro_export] macro_rules! internal_error { ($target:ty : $($other:path), *) => { $( impl From<$other> for $target { fn from(other: $other) -> Self { Self::Internal(Box::new(other)) } } )* } }