diff --git a/crates/api_models/src/errors/actix.rs b/crates/api_models/src/errors/actix.rs index 94d0bf1cc9..385c4c4401 100644 --- a/crates/api_models/src/errors/actix.rs +++ b/crates/api_models/src/errors/actix.rs @@ -14,6 +14,9 @@ impl actix_web::ResponseError for ApiErrorResponse { Self::InternalServerError(_) => StatusCode::INTERNAL_SERVER_ERROR, Self::NotImplemented(_) => StatusCode::NOT_IMPLEMENTED, Self::ConnectorError(_, code) => *code, + Self::MethodNotAllowed(_) => StatusCode::METHOD_NOT_ALLOWED, + Self::NotFound(_) => StatusCode::NOT_FOUND, + Self::BadRequest(_) => StatusCode::BAD_REQUEST, } } diff --git a/crates/api_models/src/errors/mod.rs b/crates/api_models/src/errors/mod.rs index 9ce485e652..f53250dbc5 100644 --- a/crates/api_models/src/errors/mod.rs +++ b/crates/api_models/src/errors/mod.rs @@ -1,3 +1,2 @@ pub mod actix; -pub mod serde; pub mod types; diff --git a/crates/api_models/src/errors/serde.rs b/crates/api_models/src/errors/serde.rs deleted file mode 100644 index 4a3ba0fbb8..0000000000 --- a/crates/api_models/src/errors/serde.rs +++ /dev/null @@ -1,23 +0,0 @@ -use serde::{ser::SerializeMap, Serialize}; - -use super::types::ApiErrorResponse; - -impl Serialize for ApiErrorResponse { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - let mut map = serializer.serialize_map(Some(3))?; - map.serialize_entry("error_type", self.error_type())?; - map.serialize_entry( - "error_code", - &format!( - "{}_{}", - self.get_internal_error().sub_code, - self.get_internal_error().error_identifier - ), - )?; - map.serialize_entry("error_message", self.get_internal_error().error_message)?; - map.end() - } -} diff --git a/crates/api_models/src/errors/types.rs b/crates/api_models/src/errors/types.rs index c317e7b565..ec5e5ed71b 100644 --- a/crates/api_models/src/errors/types.rs +++ b/crates/api_models/src/errors/types.rs @@ -1,19 +1,74 @@ +use std::borrow::Cow; + use reqwest::StatusCode; +#[derive(Debug, serde::Serialize)] pub enum ErrorType { InvalidRequestError, RouterError, ConnectorError, } -#[derive(Debug, serde::Serialize)] +#[derive(Debug, serde::Serialize, Clone)] pub struct ApiError { pub sub_code: &'static str, - pub error_identifier: u8, - pub error_message: &'static str, + pub error_identifier: u16, + pub error_message: String, + pub extra: Option, } -#[derive(Debug)] +impl ApiError { + pub fn new( + sub_code: &'static str, + error_identifier: u16, + error_message: impl ToString, + extra: Option, + ) -> Self { + Self { + sub_code, + error_identifier, + error_message: error_message.to_string(), + extra, + } + } +} + +#[derive(Debug, serde::Serialize)] +struct ErrorResponse<'a> { + #[serde(rename = "type")] + error_type: &'static str, + message: Cow<'a, str>, + code: String, + #[serde(flatten)] + extra: &'a Option, +} + +impl<'a> From<&'a ApiErrorResponse> for ErrorResponse<'a> { + fn from(value: &'a ApiErrorResponse) -> Self { + let error_info = value.get_internal_error(); + let error_type = value.error_type(); + Self { + code: format!("{}_{}", error_info.sub_code, error_info.error_identifier), + message: Cow::Borrowed(value.get_internal_error().error_message.as_str()), + error_type, + extra: &error_info.extra, + } + } +} + +#[derive(Debug, serde::Serialize, Default, Clone)] +pub struct Extra { + #[serde(skip_serializing_if = "Option::is_none")] + pub payment_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub data: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub connector: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub reason: Option, +} + +#[derive(Debug, Clone)] pub enum ApiErrorResponse { Unauthorized(ApiError), ForbiddenCommonResource(ApiError), @@ -24,14 +79,18 @@ pub enum ApiErrorResponse { InternalServerError(ApiError), NotImplemented(ApiError), ConnectorError(ApiError, StatusCode), + NotFound(ApiError), + MethodNotAllowed(ApiError), + BadRequest(ApiError), } impl ::core::fmt::Display for ApiErrorResponse { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let error_response: ErrorResponse<'_> = self.into(); write!( f, r#"{{"error":{}}}"#, - serde_json::to_string(self.get_internal_error()) + serde_json::to_string(&error_response) .unwrap_or_else(|_| "API error response".to_string()) ) } @@ -48,11 +107,14 @@ impl ApiErrorResponse { | Self::Unprocessable(i) | Self::InternalServerError(i) | Self::NotImplemented(i) + | Self::NotFound(i) + | Self::MethodNotAllowed(i) + | Self::BadRequest(i) | Self::ConnectorError(i, _) => i, } } - pub(crate) fn error_type(&self) -> &str { + pub(crate) fn error_type(&self) -> &'static str { match self { Self::Unauthorized(_) | Self::ForbiddenCommonResource(_) @@ -60,9 +122,14 @@ impl ApiErrorResponse { | Self::Conflict(_) | Self::Gone(_) | Self::Unprocessable(_) - | Self::NotImplemented(_) => "invalid_request", + | Self::NotImplemented(_) + | Self::MethodNotAllowed(_) + | Self::NotFound(_) + | Self::BadRequest(_) => "invalid_request", Self::InternalServerError(_) => "api", Self::ConnectorError(_, _) => "connector", } } } + +impl std::error::Error for ApiErrorResponse {} diff --git a/crates/common_utils/src/errors.rs b/crates/common_utils/src/errors.rs index e666fe4755..9a1e681211 100644 --- a/crates/common_utils/src/errors.rs +++ b/crates/common_utils/src/errors.rs @@ -85,6 +85,7 @@ where V: ErrorSwitch + error_stack::Context, U: error_stack::Context, { + #[track_caller] fn switch(self) -> Result> { match self { Ok(i) => Ok(i), diff --git a/crates/router/src/compatibility/stripe/errors.rs b/crates/router/src/compatibility/stripe/errors.rs index 9ec5a5891a..f031f2e738 100644 --- a/crates/router/src/compatibility/stripe/errors.rs +++ b/crates/router/src/compatibility/stripe/errors.rs @@ -1,7 +1,7 @@ #![allow(unused_variables)] use crate::core::errors; -#[derive(Debug, router_derive::ApiError)] +#[derive(Debug, router_derive::ApiError, Clone)] #[error(error_type_enum = StripeErrorType)] pub enum StripeErrorCode { /* @@ -529,3 +529,9 @@ impl From for StripeErrorCode { } } } + +impl common_utils::errors::ErrorSwitch for errors::ApiErrorResponse { + fn switch(&self) -> StripeErrorCode { + self.clone().into() + } +} diff --git a/crates/router/src/compatibility/wrap.rs b/crates/router/src/compatibility/wrap.rs index dc04dccf46..5452ebdc6f 100644 --- a/crates/router/src/compatibility/wrap.rs +++ b/crates/router/src/compatibility/wrap.rs @@ -1,7 +1,7 @@ use std::future::Future; use actix_web::{HttpRequest, HttpResponse, Responder}; -use error_stack::report; +use common_utils::errors::ErrorSwitch; use router_env::{instrument, tracing}; use serde::Serialize; @@ -24,11 +24,13 @@ where Fut: Future>>, Q: Serialize + std::fmt::Debug + 'a, S: From + Serialize, - E: From + Serialize + error_stack::Context + actix_web::ResponseError, + E: Serialize + error_stack::Context + actix_web::ResponseError + Clone, + errors::ApiErrorResponse: ErrorSwitch, T: std::fmt::Debug, A: AppStateInfo, { - let resp = api::server_wrap_util(state, request, payload, func, api_authentication).await; + let resp: common_utils::errors::CustomResult<_, E> = + api::server_wrap_util(state, request, payload, func, api_authentication).await; match resp { Ok(api::ApplicationResponse::Json(router_resp)) => { let pg_resp = S::try_from(router_resp); @@ -71,8 +73,7 @@ where .map_into_boxed_body(), Err(error) => { logger::error!(api_response_error=?error); - let pg_error = E::from(error.current_context().clone()); - api::log_and_return_error_response(report!(pg_error)) + api::log_and_return_error_response(error) } } } diff --git a/crates/router/src/core/errors/api_error_response.rs b/crates/router/src/core/errors/api_error_response.rs index ec49c7a841..e4adf87673 100644 --- a/crates/router/src/core/errors/api_error_response.rs +++ b/crates/router/src/core/errors/api_error_response.rs @@ -1,5 +1,8 @@ #![allow(dead_code, unused_variables)] +use api_models::errors::types::Extra; +use http::StatusCode; + #[derive(Clone, Debug, serde::Serialize)] #[serde(rename_all = "snake_case")] pub enum ErrorType { @@ -182,9 +185,7 @@ impl ::core::fmt::Display for ApiErrorResponse { } impl actix_web::ResponseError for ApiErrorResponse { - fn status_code(&self) -> reqwest::StatusCode { - use reqwest::StatusCode; - + fn status_code(&self) -> StatusCode { match self { Self::Unauthorized | Self::InvalidEphemeralKey @@ -254,3 +255,168 @@ impl actix_web::ResponseError for ApiErrorResponse { .body(self.to_string()) } } + +impl common_utils::errors::ErrorSwitch + for ApiErrorResponse +{ + fn switch(&self) -> api_models::errors::types::ApiErrorResponse { + use api_models::errors::types::{ApiError, ApiErrorResponse as AER}; + + let error_message = self.error_message(); + let error_codes = self.error_code(); + let error_type = self.error_type(); + + match self { + Self::NotImplemented { message } => { + AER::NotImplemented(ApiError::new("IR", 0, format!("{message:?}"), None)) + } + Self::Unauthorized => AER::Unauthorized(ApiError::new( + "IR", + 1, + "API key not provided or invalid API key used", None + )), + Self::InvalidRequestUrl => { + AER::NotFound(ApiError::new("IR", 2, "Unrecognized request URL", None)) + } + Self::InvalidHttpMethod => AER::MethodNotAllowed(ApiError::new( + "IR", + 3, + "The HTTP method is not applicable for this API", None + )), + Self::MissingRequiredField { field_name } => AER::BadRequest( + ApiError::new("IR", 4, format!("Missing required param: {field_name}"), None), + ), + Self::InvalidDataFormat { + field_name, + expected_format, + } => AER::Unprocessable(ApiError::new( + "IR", + 5, + format!( + "{field_name} contains invalid data. Expected format is {expected_format}" + ), None + )), + Self::InvalidRequestData { message } => { + AER::Unprocessable(ApiError::new("IR", 6, message.to_string(), None)) + } + Self::InvalidDataValue { field_name } => AER::BadRequest(ApiError::new( + "IR", + 7, + format!("Invalid value provided: {field_name}"), None + )), + Self::ClientSecretNotGiven => AER::BadRequest(ApiError::new( + "IR", + 8, + "Client secret was not provided", None + )), + Self::ClientSecretInvalid => { + AER::BadRequest(ApiError::new("IR", 9, "The client_secret provided does not match the client_secret associated with the Payment", None)) + } + Self::MandateActive => { + AER::BadRequest(ApiError::new("IR", 10, "Customer has active mandate/subsciption", None)) + } + Self::CustomerRedacted => { + AER::BadRequest(ApiError::new("IR", 11, "Customer has already been redacted", None)) + } + Self::MaximumRefundCount => AER::BadRequest(ApiError::new("IR", 12, "Reached maximum refund attempts", None)), + Self::RefundAmountExceedsPaymentAmount => { + AER::BadRequest(ApiError::new("IR", 13, "Refund amount exceeds the payment amount", None)) + } + Self::PaymentUnexpectedState { + current_flow, + field_name, + current_value, + states, + } => AER::BadRequest(ApiError::new("IR", 14, format!("This Payment could not be {current_flow} because it has a {field_name} of {current_value}. The expected state is {states}"), None)), + Self::InvalidEphemeralKey => AER::Unauthorized(ApiError::new("IR", 15, "Invalid Ephemeral Key for the customer", None)), + Self::PreconditionFailed { message } => { + AER::BadRequest(ApiError::new("IR", 16, message.to_string(), None)) + } + Self::InvalidJwtToken => AER::Unauthorized(ApiError::new("IR", 17, "Access forbidden, invalid JWT token was used", None)), + Self::GenericUnauthorized { message } => { + AER::Unauthorized(ApiError::new("IR", 18, message.to_string(), None)) + } + Self::ExternalConnectorError { + code, + message, + connector, + status_code, + } => AER::ConnectorError(ApiError::new("CE", 0, format!("{code}: {message}"), Some(Extra {connector: Some(connector.clone()), ..Default::default()})), StatusCode::from_u16(*status_code).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)), + Self::PaymentAuthorizationFailed { data } => { + AER::BadRequest(ApiError::new("CE", 1, "Payment failed during authorization with connector. Retry payment", Some(Extra { data: data.clone(), ..Default::default()}))) + } + Self::PaymentAuthenticationFailed { data } => { + AER::BadRequest(ApiError::new("CE", 2, "Payment failed during authentication with connector. Retry payment", Some(Extra { data: data.clone(), ..Default::default()}))) + } + Self::PaymentCaptureFailed { data } => { + AER::BadRequest(ApiError::new("CE", 3, "Capture attempt failed while processing with connector", Some(Extra { data: data.clone(), ..Default::default()}))) + } + Self::InvalidCardData { data } => AER::BadRequest(ApiError::new("CE", 4, "The card data is invalid", Some(Extra { data: data.clone(), ..Default::default()}))), + Self::CardExpired { data } => AER::BadRequest(ApiError::new("CE", 5, "The card has expired", Some(Extra { data: data.clone(), ..Default::default()}))), + Self::RefundFailed { data } => AER::BadRequest(ApiError::new("CE", 6, "Refund failed while processing with connector. Retry refund", Some(Extra { data: data.clone(), ..Default::default()}))), + Self::VerificationFailed { data } => { + AER::BadRequest(ApiError::new("CE", 7, "Verification failed while processing with connector. Retry operation", Some(Extra { data: data.clone(), ..Default::default()}))) + } + Self::InternalServerError => { + AER::InternalServerError(ApiError::new("HE", 0, "Something went wrong", None)) + } + Self::DuplicateRefundRequest => AER::BadRequest(ApiError::new("HE", 1, "Duplicate refund request. Refund already attempted with the refund ID", None)), + Self::DuplicateMandate => AER::BadRequest(ApiError::new("HE", 1, "Duplicate mandate request. Mandate already attempted with the Mandate ID", None)), + Self::DuplicateMerchantAccount => AER::BadRequest(ApiError::new("HE", 1, "The merchant account with the specified details already exists in our records", None)), + Self::DuplicateMerchantConnectorAccount => { + AER::BadRequest(ApiError::new("HE", 1, "The merchant connector account with the specified details already exists in our records", None)) + } + Self::DuplicatePaymentMethod => AER::BadRequest(ApiError::new("HE", 1, "The payment method with the specified details already exists in our records", None)), + Self::DuplicatePayment { payment_id } => { + AER::BadRequest(ApiError::new("HE", 1, format!("The payment with the specified payment_id '{payment_id}' already exists in our records"), None)) + } + Self::RefundNotFound => { + AER::NotFound(ApiError::new("HE", 2, "Refund does not exist in our records.", None)) + } + Self::CustomerNotFound => { + AER::NotFound(ApiError::new("HE", 2, "Customer does not exist in our records", None)) + } + Self::ConfigNotFound => { + AER::NotFound(ApiError::new("HE", 2, "Config key does not exist in our records.", None)) + } + Self::PaymentNotFound => { + AER::NotFound(ApiError::new("HE", 2, "Payment does not exist in our records", None)) + } + Self::PaymentMethodNotFound => { + AER::NotFound(ApiError::new("HE", 2, "Payment method does not exist in our records", None)) + } + Self::MerchantAccountNotFound => { + AER::NotFound(ApiError::new("HE", 2, "Merchant account does not exist in our records", None)) + } + Self::MerchantConnectorAccountNotFound => { + AER::NotFound(ApiError::new("HE", 2, "Merchant connector account does not exist in our records", None)) + } + Self::ResourceIdNotFound => { + AER::NotFound(ApiError::new("HE", 2, "Resource ID does not exist in our records", None)) + } + Self::MandateNotFound => { + AER::NotFound(ApiError::new("HE", 2, "Mandate does not exist in our records", None)) + } + Self::ReturnUrlUnavailable => AER::NotFound(ApiError::new("HE", 3, "Return URL is not configured and not passed in payments request", None)), + Self::RefundNotPossible { connector } => { + AER::BadRequest(ApiError::new("HE", 3, "This refund is not possible through Hyperswitch. Please raise the refund through {connector} dashboard", None)) + } + Self::MandateValidationFailed { reason } => { + AER::BadRequest(ApiError::new("HE", 3, "Mandate Validation Failed", Some(Extra { reason: Some(reason.clone()), ..Default::default() }))) + } + Self::PaymentNotSucceeded => AER::BadRequest(ApiError::new("HE", 3, "The payment has not succeeded yet. Please pass a successful payment to initiate refund", None)), + Self::SuccessfulPaymentNotFound => { + AER::NotFound(ApiError::new("HE", 4, "Successful payment not found for the given payment id", None)) + } + Self::IncorrectConnectorNameGiven => { + AER::NotFound(ApiError::new("HE", 4, "The connector provided in the request is incorrect or not available", None)) + } + Self::AddressNotFound => { + AER::NotFound(ApiError::new("HE", 4, "Address does not exist in our records", None)) + }, + Self::ApiKeyNotFound => { + AER::NotFound(ApiError::new("HE", 2, "API Key does not exist in our records", None)) + } + } + } +} diff --git a/crates/router/src/core/errors/error_handlers.rs b/crates/router/src/core/errors/error_handlers.rs index 7830ab71ec..716a81ec28 100644 --- a/crates/router/src/core/errors/error_handlers.rs +++ b/crates/router/src/core/errors/error_handlers.rs @@ -1,8 +1,9 @@ -use actix_web::{dev::ServiceResponse, middleware::ErrorHandlerResponse, ResponseError}; +use actix_web::{body, dev::ServiceResponse, middleware::ErrorHandlerResponse, ResponseError}; use http::StatusCode; use super::ApiErrorResponse; -pub fn custom_error_handlers( +use crate::logger; +pub fn custom_error_handlers( res: ServiceResponse, ) -> actix_web::Result> { let error_response = match res.status() { @@ -10,15 +11,20 @@ pub fn custom_error_handlers( StatusCode::METHOD_NOT_ALLOWED => ApiErrorResponse::InvalidHttpMethod, _ => ApiErrorResponse::InternalServerError, }; - let (req, _) = res.into_parts(); - let res = error_response.error_response(); + + let (req, res) = res.into_parts(); + logger::warn!(error_response=?res); + let res = match res.error() { + Some(_) => res.map_into_boxed_body(), + None => error_response.error_response(), + }; let res = ServiceResponse::new(req, res) .map_into_boxed_body() .map_into_right_body(); Ok(ErrorHandlerResponse::Response(res)) } -// can be used as .default_service for web::resource to modify the default behaviour of method_not_found error i.e. raised +// can be used as .default_service for web::resource to modify the default behavior of method_not_found error i.e. raised // use actix_web::dev::ServiceRequest // pub async fn default_service_405(req: ServiceRequest) -> Result { // Ok(req.into_response(ApiErrorResponse::InvalidHttpMethod.error_response())) diff --git a/crates/router/src/core/errors/utils.rs b/crates/router/src/core/errors/utils.rs index b05dd25b15..d146dd0dd6 100644 --- a/crates/router/src/core/errors/utils.rs +++ b/crates/router/src/core/errors/utils.rs @@ -15,6 +15,7 @@ pub trait StorageErrorExt { } impl StorageErrorExt for error_stack::Report { + #[track_caller] fn to_not_found_response( self, not_found_response: errors::ApiErrorResponse, diff --git a/crates/router/src/services/api.rs b/crates/router/src/services/api.rs index 8a0b531809..e3cbb51288 100644 --- a/crates/router/src/services/api.rs +++ b/crates/router/src/services/api.rs @@ -10,6 +10,7 @@ use std::{ }; use actix_web::{body, HttpRequest, HttpResponse, Responder}; +use common_utils::errors::ReportSwitchExt; use error_stack::{report, IntoReport, Report, ResultExt}; use masking::ExposeOptionInterface; use router_env::{instrument, tracing, Tag}; @@ -20,7 +21,7 @@ pub use self::request::{Method, Request, RequestBuilder}; use crate::{ configs::settings::Connectors, core::{ - errors::{self, CustomResult, RouterResponse, RouterResult}, + errors::{self, CustomResult, RouterResult}, payments, }, db::StorageInterface, @@ -384,31 +385,34 @@ pub enum AuthFlow { } #[instrument(skip(request, payload, state, func, api_auth))] -pub async fn server_wrap_util<'a, 'b, A, U, T, Q, F, Fut>( +pub async fn server_wrap_util<'a, 'b, A, U, T, Q, F, Fut, E, OErr>( state: &'b A, request: &'a HttpRequest, payload: T, func: F, api_auth: &dyn auth::AuthenticateAndFetch, -) -> RouterResult> +) -> CustomResult, OErr> where F: Fn(&'b A, U, T) -> Fut, - Fut: Future>, + Fut: Future, E>>, Q: Serialize + Debug + 'a, T: Debug, A: AppStateInfo, + CustomResult, E>: ReportSwitchExt, OErr>, + CustomResult: ReportSwitchExt, { let auth_out = api_auth .authenticate_and_fetch(request.headers(), state) - .await?; - func(state, auth_out, payload).await + .await + .switch()?; + func(state, auth_out, payload).await.switch() } #[instrument( skip(request, payload, state, func, api_auth), fields(request_method, request_url_path) )] -pub async fn server_wrap<'a, 'b, A, T, U, Q, F, Fut>( +pub async fn server_wrap<'a, 'b, A, T, U, Q, F, Fut, E>( state: &'b A, request: &'a HttpRequest, payload: T, @@ -417,10 +421,12 @@ pub async fn server_wrap<'a, 'b, A, T, U, Q, F, Fut>( ) -> HttpResponse where F: Fn(&'b A, U, T) -> Fut, - Fut: Future>>, + Fut: Future, E>>, Q: Serialize + Debug + 'a, T: Debug, A: AppStateInfo, + CustomResult, E>: + ReportSwitchExt, api_models::errors::types::ApiErrorResponse>, { let request_method = request.method().as_str(); let url_path = request.path(); @@ -476,10 +482,10 @@ where pub fn log_and_return_error_response(error: Report) -> HttpResponse where - T: actix_web::ResponseError + error_stack::Context, + T: actix_web::ResponseError + error_stack::Context + Clone, { logger::error!(?error); - error.current_context().error_response() + HttpResponse::from_error(error.current_context().clone()) } pub async fn authenticate_by_api_key(