feat(api_models): add error structs (#532)

Co-authored-by: Sanchith Hegde <22217505+SanchithHegde@users.noreply.github.com>
This commit is contained in:
Nishant Joshi
2023-02-16 14:49:14 +05:30
committed by GitHub
parent 326d6bebe1
commit d107b44fd3
11 changed files with 288 additions and 55 deletions

View File

@ -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<serde_qs::Error> for StripeErrorCode {
}
}
}
impl common_utils::errors::ErrorSwitch<StripeErrorCode> for errors::ApiErrorResponse {
fn switch(&self) -> StripeErrorCode {
self.clone().into()
}
}

View File

@ -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<Output = RouterResult<api::ApplicationResponse<Q>>>,
Q: Serialize + std::fmt::Debug + 'a,
S: From<Q> + Serialize,
E: From<errors::ApiErrorResponse> + Serialize + error_stack::Context + actix_web::ResponseError,
E: Serialize + error_stack::Context + actix_web::ResponseError + Clone,
errors::ApiErrorResponse: ErrorSwitch<E>,
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)
}
}
}

View File

@ -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<api_models::errors::types::ApiErrorResponse>
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))
}
}
}
}

View File

@ -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<B>(
use crate::logger;
pub fn custom_error_handlers<B: body::MessageBody + 'static>(
res: ServiceResponse<B>,
) -> actix_web::Result<ErrorHandlerResponse<B>> {
let error_response = match res.status() {
@ -10,15 +11,20 @@ pub fn custom_error_handlers<B>(
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<E>(req: ServiceRequest) -> Result<ServiceResponse, E> {
// Ok(req.into_response(ApiErrorResponse::InvalidHttpMethod.error_response()))

View File

@ -15,6 +15,7 @@ pub trait StorageErrorExt {
}
impl StorageErrorExt for error_stack::Report<errors::StorageError> {
#[track_caller]
fn to_not_found_response(
self,
not_found_response: errors::ApiErrorResponse,

View File

@ -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<U, A>,
) -> RouterResult<ApplicationResponse<Q>>
) -> CustomResult<ApplicationResponse<Q>, OErr>
where
F: Fn(&'b A, U, T) -> Fut,
Fut: Future<Output = RouterResponse<Q>>,
Fut: Future<Output = CustomResult<ApplicationResponse<Q>, E>>,
Q: Serialize + Debug + 'a,
T: Debug,
A: AppStateInfo,
CustomResult<ApplicationResponse<Q>, E>: ReportSwitchExt<ApplicationResponse<Q>, OErr>,
CustomResult<U, errors::ApiErrorResponse>: ReportSwitchExt<U, OErr>,
{
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<Output = RouterResult<ApplicationResponse<Q>>>,
Fut: Future<Output = CustomResult<ApplicationResponse<Q>, E>>,
Q: Serialize + Debug + 'a,
T: Debug,
A: AppStateInfo,
CustomResult<ApplicationResponse<Q>, E>:
ReportSwitchExt<ApplicationResponse<Q>, 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<T>(error: Report<T>) -> 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(