From 4b45d21269437479435302aa1ea7d3d741e2a009 Mon Sep 17 00:00:00 2001 From: Sakil Mostak <73734619+Sakilmostak@users.noreply.github.com> Date: Wed, 27 Nov 2024 20:35:53 +0530 Subject: [PATCH] refactor(core): add error handling wrapper to wehbook (#6636) --- crates/api_models/src/connector_enums.rs | 3 + .../src/connectors/cashtocode.rs | 3 +- .../src/connectors/worldline.rs | 3 +- .../src/connectors/zen.rs | 3 +- .../src/connectors/zsl.rs | 3 +- crates/hyperswitch_interfaces/src/webhooks.rs | 29 +++++- crates/router/src/connector/adyen.rs | 2 + crates/router/src/connector/adyenplatform.rs | 31 +++++-- crates/router/src/connector/braintree.rs | 2 + crates/router/src/core/webhooks/incoming.rs | 93 +++++++++++++++---- .../router/src/core/webhooks/incoming_v2.rs | 4 +- .../connector_integration_interface.rs | 8 +- 12 files changed, 151 insertions(+), 33 deletions(-) diff --git a/crates/api_models/src/connector_enums.rs b/crates/api_models/src/connector_enums.rs index 783ecb12b48d..3d027c026d7e 100644 --- a/crates/api_models/src/connector_enums.rs +++ b/crates/api_models/src/connector_enums.rs @@ -282,6 +282,9 @@ impl Connector { pub fn is_pre_processing_required_before_authorize(&self) -> bool { matches!(self, Self::Airwallex) } + pub fn should_acknowledge_webhook_for_resource_not_found_errors(&self) -> bool { + matches!(self, Self::Adyenplatform) + } #[cfg(feature = "dummy_connector")] pub fn validate_dummy_connector_enabled( &self, diff --git a/crates/hyperswitch_connectors/src/connectors/cashtocode.rs b/crates/hyperswitch_connectors/src/connectors/cashtocode.rs index c72b63aebc05..1ee6f10c6350 100644 --- a/crates/hyperswitch_connectors/src/connectors/cashtocode.rs +++ b/crates/hyperswitch_connectors/src/connectors/cashtocode.rs @@ -30,7 +30,7 @@ use hyperswitch_interfaces::{ errors, events::connector_api_logs::ConnectorEvent, types::{PaymentsAuthorizeType, Response}, - webhooks, + webhooks::{self, IncomingWebhookFlowError}, }; use masking::{Mask, PeekInterface, Secret}; use transformers as cashtocode; @@ -420,6 +420,7 @@ impl webhooks::IncomingWebhook for Cashtocode { fn get_webhook_api_response( &self, request: &webhooks::IncomingWebhookRequestDetails<'_>, + _error_kind: Option, ) -> CustomResult, errors::ConnectorError> { let status = "EXECUTED".to_string(); let obj: transformers::CashtocodePaymentsSyncResponse = request diff --git a/crates/hyperswitch_connectors/src/connectors/worldline.rs b/crates/hyperswitch_connectors/src/connectors/worldline.rs index c5a8466175c1..e40d48172243 100644 --- a/crates/hyperswitch_connectors/src/connectors/worldline.rs +++ b/crates/hyperswitch_connectors/src/connectors/worldline.rs @@ -41,7 +41,7 @@ use hyperswitch_interfaces::{ PaymentsAuthorizeType, PaymentsCaptureType, PaymentsSyncType, PaymentsVoidType, RefundExecuteType, RefundSyncType, Response, }, - webhooks, + webhooks::{self, IncomingWebhookFlowError}, }; use masking::{ExposeInterface, Mask, PeekInterface}; use ring::hmac; @@ -814,6 +814,7 @@ impl webhooks::IncomingWebhook for Worldline { fn get_webhook_api_response( &self, request: &webhooks::IncomingWebhookRequestDetails<'_>, + _error_kind: Option, ) -> CustomResult< hyperswitch_domain_models::api::ApplicationResponse, errors::ConnectorError, diff --git a/crates/hyperswitch_connectors/src/connectors/zen.rs b/crates/hyperswitch_connectors/src/connectors/zen.rs index a90b10fbd866..5dc9e4d3176d 100644 --- a/crates/hyperswitch_connectors/src/connectors/zen.rs +++ b/crates/hyperswitch_connectors/src/connectors/zen.rs @@ -41,7 +41,7 @@ use hyperswitch_interfaces::{ errors, events::connector_api_logs::ConnectorEvent, types::{PaymentsAuthorizeType, PaymentsSyncType, RefundExecuteType, RefundSyncType, Response}, - webhooks::{IncomingWebhook, IncomingWebhookRequestDetails}, + webhooks::{IncomingWebhook, IncomingWebhookFlowError, IncomingWebhookRequestDetails}, }; use masking::{Mask, PeekInterface, Secret}; use transformers::{self as zen, ZenPaymentStatus, ZenWebhookTxnType}; @@ -671,6 +671,7 @@ impl IncomingWebhook for Zen { fn get_webhook_api_response( &self, _request: &IncomingWebhookRequestDetails<'_>, + _error_kind: Option, ) -> CustomResult, errors::ConnectorError> { Ok(ApplicationResponse::Json(serde_json::json!({ "status": "ok" diff --git a/crates/hyperswitch_connectors/src/connectors/zsl.rs b/crates/hyperswitch_connectors/src/connectors/zsl.rs index 0a833a1f84ad..13007b72601e 100644 --- a/crates/hyperswitch_connectors/src/connectors/zsl.rs +++ b/crates/hyperswitch_connectors/src/connectors/zsl.rs @@ -36,7 +36,7 @@ use hyperswitch_interfaces::{ errors, events::connector_api_logs::ConnectorEvent, types::{self, Response}, - webhooks::{IncomingWebhook, IncomingWebhookRequestDetails}, + webhooks::{IncomingWebhook, IncomingWebhookFlowError, IncomingWebhookRequestDetails}, }; use masking::{ExposeInterface, Secret}; use transformers::{self as zsl, get_status}; @@ -442,6 +442,7 @@ impl IncomingWebhook for Zsl { fn get_webhook_api_response( &self, _request: &IncomingWebhookRequestDetails<'_>, + _error_kind: Option, ) -> CustomResult, errors::ConnectorError> { Ok(ApplicationResponse::TextPlain("CALLBACK-OK".to_string())) } diff --git a/crates/hyperswitch_interfaces/src/webhooks.rs b/crates/hyperswitch_interfaces/src/webhooks.rs index f5240aed9ca5..9a1a3f997932 100644 --- a/crates/hyperswitch_interfaces/src/webhooks.rs +++ b/crates/hyperswitch_interfaces/src/webhooks.rs @@ -2,7 +2,9 @@ use common_utils::{crypto, errors::CustomResult, ext_traits::ValueExt}; use error_stack::ResultExt; -use hyperswitch_domain_models::api::ApplicationResponse; +use hyperswitch_domain_models::{ + api::ApplicationResponse, errors::api_error_response::ApiErrorResponse, +}; use masking::{ExposeInterface, Secret}; use crate::{api::ConnectorCommon, errors}; @@ -22,6 +24,30 @@ pub struct IncomingWebhookRequestDetails<'a> { pub query_params: String, } +/// IncomingWebhookFlowError enum defining the error type for incoming webhook +#[derive(Debug)] +pub enum IncomingWebhookFlowError { + /// Resource not found for the webhook + ResourceNotFound, + /// Internal error for the webhook + InternalError, +} + +impl From<&ApiErrorResponse> for IncomingWebhookFlowError { + fn from(api_error_response: &ApiErrorResponse) -> Self { + match api_error_response { + ApiErrorResponse::WebhookResourceNotFound + | ApiErrorResponse::DisputeNotFound { .. } + | ApiErrorResponse::PayoutNotFound + | ApiErrorResponse::MandateNotFound + | ApiErrorResponse::PaymentNotFound + | ApiErrorResponse::RefundNotFound + | ApiErrorResponse::AuthenticationNotFound { .. } => Self::ResourceNotFound, + _ => Self::InternalError, + } + } +} + /// Trait defining incoming webhook #[async_trait::async_trait] pub trait IncomingWebhook: ConnectorCommon + Sync { @@ -203,6 +229,7 @@ pub trait IncomingWebhook: ConnectorCommon + Sync { fn get_webhook_api_response( &self, _request: &IncomingWebhookRequestDetails<'_>, + _error_kind: Option, ) -> CustomResult, errors::ConnectorError> { Ok(ApplicationResponse::StatusOk) } diff --git a/crates/router/src/connector/adyen.rs b/crates/router/src/connector/adyen.rs index c9ccd8a9c629..f3b7414a923a 100644 --- a/crates/router/src/connector/adyen.rs +++ b/crates/router/src/connector/adyen.rs @@ -7,6 +7,7 @@ use common_utils::{ }; use diesel_models::{enums as storage_enums, enums}; use error_stack::{report, ResultExt}; +use hyperswitch_interfaces::webhooks::IncomingWebhookFlowError; use masking::{ExposeInterface, Secret}; use ring::hmac; use router_env::{instrument, tracing}; @@ -1880,6 +1881,7 @@ impl api::IncomingWebhook for Adyen { fn get_webhook_api_response( &self, _request: &api::IncomingWebhookRequestDetails<'_>, + _error_kind: Option, ) -> CustomResult, errors::ConnectorError> { Ok(services::api::ApplicationResponse::TextPlain( diff --git a/crates/router/src/connector/adyenplatform.rs b/crates/router/src/connector/adyenplatform.rs index 3da1a2d33be2..2ee80699a5a0 100644 --- a/crates/router/src/connector/adyenplatform.rs +++ b/crates/router/src/connector/adyenplatform.rs @@ -13,6 +13,8 @@ use error_stack::report; use error_stack::ResultExt; #[cfg(feature = "payouts")] use http::HeaderName; +use hyperswitch_interfaces::webhooks::IncomingWebhookFlowError; +use masking::Maskable; #[cfg(feature = "payouts")] use masking::Secret; #[cfg(feature = "payouts")] @@ -27,11 +29,7 @@ use crate::{ configs::settings, core::errors::{self, CustomResult}, headers, - services::{ - self, - request::{self, Mask}, - ConnectorValidation, - }, + services::{self, request::Mask, ConnectorValidation}, types::{ self, api::{self, ConnectorCommon}, @@ -67,7 +65,7 @@ impl ConnectorCommon for Adyenplatform { fn get_auth_header( &self, auth_type: &types::ConnectorAuthType, - ) -> CustomResult)>, errors::ConnectorError> { + ) -> CustomResult)>, errors::ConnectorError> { let auth = adyenplatform::AdyenplatformAuthType::try_from(auth_type) .change_context(errors::ConnectorError::FailedToObtainAuthType)?; Ok(vec![( @@ -209,7 +207,7 @@ impl services::ConnectorIntegration, _connectors: &settings::Connectors, - ) -> CustomResult)>, errors::ConnectorError> { + ) -> CustomResult)>, errors::ConnectorError> { let mut header = vec![( headers::CONTENT_TYPE.to_string(), types::PayoutFulfillType::get_content_type(self) @@ -401,6 +399,25 @@ impl api::IncomingWebhook for Adyenplatform { } } + fn get_webhook_api_response( + &self, + _request: &api::IncomingWebhookRequestDetails<'_>, + error_kind: Option, + ) -> CustomResult, errors::ConnectorError> + { + if error_kind.is_some() { + Ok(services::api::ApplicationResponse::JsonWithHeaders(( + serde_json::Value::Null, + vec![( + "x-http-code".to_string(), + Maskable::Masked(Secret::new("404".to_string())), + )], + ))) + } else { + Ok(services::api::ApplicationResponse::StatusOk) + } + } + fn get_webhook_event_type( &self, #[cfg(feature = "payouts")] request: &api::IncomingWebhookRequestDetails<'_>, diff --git a/crates/router/src/connector/braintree.rs b/crates/router/src/connector/braintree.rs index dba261218801..40155f7117f5 100644 --- a/crates/router/src/connector/braintree.rs +++ b/crates/router/src/connector/braintree.rs @@ -10,6 +10,7 @@ use common_utils::{ }; use diesel_models::enums; use error_stack::{report, Report, ResultExt}; +use hyperswitch_interfaces::webhooks::IncomingWebhookFlowError; use masking::{ExposeInterface, PeekInterface, Secret}; use ring::hmac; use sha1::{Digest, Sha1}; @@ -980,6 +981,7 @@ impl api::IncomingWebhook for Braintree { fn get_webhook_api_response( &self, _request: &api::IncomingWebhookRequestDetails<'_>, + _error_kind: Option, ) -> CustomResult, errors::ConnectorError> { Ok(services::api::ApplicationResponse::TextPlain( diff --git a/crates/router/src/core/webhooks/incoming.rs b/crates/router/src/core/webhooks/incoming.rs index 3532f1e3fd72..5eb489da788e 100644 --- a/crates/router/src/core/webhooks/incoming.rs +++ b/crates/router/src/core/webhooks/incoming.rs @@ -12,7 +12,7 @@ use hyperswitch_domain_models::{ router_request_types::VerifyWebhookSourceRequestData, router_response_types::{VerifyWebhookSourceResponseData, VerifyWebhookStatus}, }; -use hyperswitch_interfaces::webhooks::IncomingWebhookRequestDetails; +use hyperswitch_interfaces::webhooks::{IncomingWebhookFlowError, IncomingWebhookRequestDetails}; use masking::{ExposeInterface, PeekInterface}; use router_env::{instrument, metrics::add_attributes, tracing, tracing_actix_web::RequestId}; @@ -209,7 +209,7 @@ async fn incoming_webhooks_core( ); let response = connector - .get_webhook_api_response(&request_details) + .get_webhook_api_response(&request_details, None) .switch() .attach_printable("Failed while early return in case of event type parsing")?; @@ -260,14 +260,25 @@ async fn incoming_webhooks_core( let merchant_connector_account = match merchant_connector_account { Some(merchant_connector_account) => merchant_connector_account, None => { - Box::pin(helper_utils::get_mca_from_object_reference_id( + match Box::pin(helper_utils::get_mca_from_object_reference_id( &state, object_ref_id.clone(), &merchant_account, &connector_name, &key_store, )) - .await? + .await + { + Ok(mca) => mca, + Err(error) => { + return handle_incoming_webhook_error( + error, + &connector, + connector_name.as_str(), + &request_details, + ); + } + } } }; @@ -358,7 +369,7 @@ async fn incoming_webhooks_core( id: profile_id.get_string_repr().to_owned(), })?; - match flow_type { + let result_response = match flow_type { api::WebhookFlow::Payment => Box::pin(payments_incoming_webhook_flow( state.clone(), req_state, @@ -372,7 +383,7 @@ async fn incoming_webhooks_core( event_type, )) .await - .attach_printable("Incoming webhook flow for payments failed")?, + .attach_printable("Incoming webhook flow for payments failed"), api::WebhookFlow::Refund => Box::pin(refunds_incoming_webhook_flow( state.clone(), @@ -385,7 +396,7 @@ async fn incoming_webhooks_core( event_type, )) .await - .attach_printable("Incoming webhook flow for refunds failed")?, + .attach_printable("Incoming webhook flow for refunds failed"), api::WebhookFlow::Dispute => Box::pin(disputes_incoming_webhook_flow( state.clone(), @@ -399,7 +410,7 @@ async fn incoming_webhooks_core( event_type, )) .await - .attach_printable("Incoming webhook flow for disputes failed")?, + .attach_printable("Incoming webhook flow for disputes failed"), api::WebhookFlow::BankTransfer => Box::pin(bank_transfer_webhook_flow( state.clone(), @@ -411,9 +422,9 @@ async fn incoming_webhooks_core( source_verified, )) .await - .attach_printable("Incoming bank-transfer webhook flow failed")?, + .attach_printable("Incoming bank-transfer webhook flow failed"), - api::WebhookFlow::ReturnResponse => WebhookResponseTracker::NoEffect, + api::WebhookFlow::ReturnResponse => Ok(WebhookResponseTracker::NoEffect), api::WebhookFlow::Mandate => Box::pin(mandates_incoming_webhook_flow( state.clone(), @@ -425,7 +436,7 @@ async fn incoming_webhooks_core( event_type, )) .await - .attach_printable("Incoming webhook flow for mandates failed")?, + .attach_printable("Incoming webhook flow for mandates failed"), api::WebhookFlow::ExternalAuthentication => { Box::pin(external_authentication_incoming_webhook_flow( @@ -442,7 +453,7 @@ async fn incoming_webhooks_core( merchant_connector_account, )) .await - .attach_printable("Incoming webhook flow for external authentication failed")? + .attach_printable("Incoming webhook flow for external authentication failed") } api::WebhookFlow::FraudCheck => Box::pin(frm_incoming_webhook_flow( state.clone(), @@ -455,7 +466,7 @@ async fn incoming_webhooks_core( business_profile, )) .await - .attach_printable("Incoming webhook flow for fraud check failed")?, + .attach_printable("Incoming webhook flow for fraud check failed"), #[cfg(feature = "payouts")] api::WebhookFlow::Payout => Box::pin(payouts_incoming_webhook_flow( @@ -468,10 +479,22 @@ async fn incoming_webhooks_core( source_verified, )) .await - .attach_printable("Incoming webhook flow for payouts failed")?, + .attach_printable("Incoming webhook flow for payouts failed"), _ => Err(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Unsupported Flow Type received in incoming webhooks")?, + .attach_printable("Unsupported Flow Type received in incoming webhooks"), + }; + + match result_response { + Ok(response) => response, + Err(error) => { + return handle_incoming_webhook_error( + error, + &connector, + connector_name.as_str(), + &request_details, + ); + } } } else { metrics::WEBHOOK_INCOMING_FILTERED_COUNT.add( @@ -486,7 +509,7 @@ async fn incoming_webhooks_core( }; let response = connector - .get_webhook_api_response(&request_details) + .get_webhook_api_response(&request_details, None) .switch() .attach_printable("Could not get incoming webhook api response from connector")?; @@ -497,6 +520,44 @@ async fn incoming_webhooks_core( Ok((response, webhook_effect, serialized_request)) } +fn handle_incoming_webhook_error( + error: error_stack::Report, + connector: &ConnectorEnum, + connector_name: &str, + request_details: &IncomingWebhookRequestDetails<'_>, +) -> errors::RouterResult<( + services::ApplicationResponse, + WebhookResponseTracker, + serde_json::Value, +)> { + logger::error!(?error, "Incoming webhook flow failed"); + + // fetch the connector enum from the connector name + let connector_enum = api_models::connector_enums::Connector::from_str(connector_name) + .change_context(errors::ApiErrorResponse::InvalidDataValue { + field_name: "connector", + }) + .attach_printable_lazy(|| format!("unable to parse connector name {connector_name:?}"))?; + + // get the error response from the connector + if connector_enum.should_acknowledge_webhook_for_resource_not_found_errors() { + let response = connector + .get_webhook_api_response( + request_details, + Some(IncomingWebhookFlowError::from(error.current_context())), + ) + .switch() + .attach_printable("Failed to get incoming webhook api response from connector")?; + Ok(( + response, + WebhookResponseTracker::NoEffect, + serde_json::Value::Null, + )) + } else { + Err(error) + } +} + #[allow(clippy::too_many_arguments)] #[instrument(skip_all)] async fn payments_incoming_webhook_flow( diff --git a/crates/router/src/core/webhooks/incoming_v2.rs b/crates/router/src/core/webhooks/incoming_v2.rs index 0deb91efaa8d..c1734794fcc7 100644 --- a/crates/router/src/core/webhooks/incoming_v2.rs +++ b/crates/router/src/core/webhooks/incoming_v2.rs @@ -192,7 +192,7 @@ async fn incoming_webhooks_core( ); let response = connector - .get_webhook_api_response(&request_details) + .get_webhook_api_response(&request_details, None) .switch() .attach_printable("Failed while early return in case of event type parsing")?; @@ -367,7 +367,7 @@ async fn incoming_webhooks_core( }; let response = connector - .get_webhook_api_response(&request_details) + .get_webhook_api_response(&request_details, None) .switch() .attach_printable("Could not get incoming webhook api response from connector")?; diff --git a/crates/router/src/services/connector_integration_interface.rs b/crates/router/src/services/connector_integration_interface.rs index 53690227f592..ccbf65917f1a 100644 --- a/crates/router/src/services/connector_integration_interface.rs +++ b/crates/router/src/services/connector_integration_interface.rs @@ -1,7 +1,8 @@ use common_utils::{crypto, errors::CustomResult, request::Request}; use hyperswitch_domain_models::{router_data::RouterData, router_data_v2::RouterDataV2}; use hyperswitch_interfaces::{ - authentication::ExternalAuthenticationPayload, connector_integration_v2::ConnectorIntegrationV2, + authentication::ExternalAuthenticationPayload, + connector_integration_v2::ConnectorIntegrationV2, webhooks::IncomingWebhookFlowError, }; use super::{BoxedConnectorIntegrationV2, ConnectorValidation}; @@ -279,11 +280,12 @@ impl api::IncomingWebhook for ConnectorEnum { fn get_webhook_api_response( &self, request: &IncomingWebhookRequestDetails<'_>, + error_kind: Option, ) -> CustomResult, errors::ConnectorError> { match self { - Self::Old(connector) => connector.get_webhook_api_response(request), - Self::New(connector) => connector.get_webhook_api_response(request), + Self::Old(connector) => connector.get_webhook_api_response(request, error_kind), + Self::New(connector) => connector.get_webhook_api_response(request, error_kind), } }