mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-29 17:19:15 +08:00
feat(api_models): add error structs (#532)
Co-authored-by: Sanchith Hegde <22217505+SanchithHegde@users.noreply.github.com>
This commit is contained in:
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()))
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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(
|
||||
|
||||
Reference in New Issue
Block a user