mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-11-02 21:07:58 +08:00
fix(webhooks): abort outgoing webhook retry task if webhook URL is not available in business profile (#3997)
This commit is contained in:
@ -259,45 +259,44 @@ pub enum WebhooksFlowError {
|
|||||||
#[error("Webhook details for merchant not configured")]
|
#[error("Webhook details for merchant not configured")]
|
||||||
MerchantWebhookDetailsNotFound,
|
MerchantWebhookDetailsNotFound,
|
||||||
#[error("Merchant does not have a webhook URL configured")]
|
#[error("Merchant does not have a webhook URL configured")]
|
||||||
MerchantWebhookURLNotConfigured,
|
MerchantWebhookUrlNotConfigured,
|
||||||
#[error("Payments core flow failed")]
|
|
||||||
PaymentsCoreFailed,
|
|
||||||
#[error("Refunds core flow failed")]
|
|
||||||
RefundsCoreFailed,
|
|
||||||
#[error("Dispuste core flow failed")]
|
|
||||||
DisputeCoreFailed,
|
|
||||||
#[error("Webhook event creation failed")]
|
|
||||||
WebhookEventCreationFailed,
|
|
||||||
#[error("Webhook event updation failed")]
|
#[error("Webhook event updation failed")]
|
||||||
WebhookEventUpdationFailed,
|
WebhookEventUpdationFailed,
|
||||||
#[error("Outgoing webhook body signing failed")]
|
#[error("Outgoing webhook body signing failed")]
|
||||||
OutgoingWebhookSigningFailed,
|
OutgoingWebhookSigningFailed,
|
||||||
#[error("Unable to fork webhooks flow for outgoing webhooks")]
|
|
||||||
ForkFlowFailed,
|
|
||||||
#[error("Webhook api call to merchant failed")]
|
#[error("Webhook api call to merchant failed")]
|
||||||
CallToMerchantFailed,
|
CallToMerchantFailed,
|
||||||
#[error("Webhook not received by merchant")]
|
#[error("Webhook not received by merchant")]
|
||||||
NotReceivedByMerchant,
|
NotReceivedByMerchant,
|
||||||
#[error("Resource not found")]
|
|
||||||
ResourceNotFound,
|
|
||||||
#[error("Webhook source verification failed")]
|
|
||||||
WebhookSourceVerificationFailed,
|
|
||||||
#[error("Webhook event object creation failed")]
|
|
||||||
WebhookEventObjectCreationFailed,
|
|
||||||
#[error("Not implemented")]
|
|
||||||
NotImplemented,
|
|
||||||
#[error("Dispute webhook status validation failed")]
|
#[error("Dispute webhook status validation failed")]
|
||||||
DisputeWebhookValidationFailed,
|
DisputeWebhookValidationFailed,
|
||||||
#[error("Outgoing webhook body encoding failed")]
|
#[error("Outgoing webhook body encoding failed")]
|
||||||
OutgoingWebhookEncodingFailed,
|
OutgoingWebhookEncodingFailed,
|
||||||
#[error("Missing required field: {field_name}")]
|
|
||||||
MissingRequiredField { field_name: &'static str },
|
|
||||||
#[error("Failed to update outgoing webhook process tracker task")]
|
#[error("Failed to update outgoing webhook process tracker task")]
|
||||||
OutgoingWebhookProcessTrackerTaskUpdateFailed,
|
OutgoingWebhookProcessTrackerTaskUpdateFailed,
|
||||||
#[error("Failed to schedule retry attempt for outgoing webhook")]
|
#[error("Failed to schedule retry attempt for outgoing webhook")]
|
||||||
OutgoingWebhookRetrySchedulingFailed,
|
OutgoingWebhookRetrySchedulingFailed,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl WebhooksFlowError {
|
||||||
|
pub(crate) fn is_webhook_delivery_retryable_error(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::MerchantConfigNotFound
|
||||||
|
| Self::MerchantWebhookDetailsNotFound
|
||||||
|
| Self::MerchantWebhookUrlNotConfigured => false,
|
||||||
|
|
||||||
|
Self::WebhookEventUpdationFailed
|
||||||
|
| Self::OutgoingWebhookSigningFailed
|
||||||
|
| Self::CallToMerchantFailed
|
||||||
|
| Self::NotReceivedByMerchant
|
||||||
|
| Self::DisputeWebhookValidationFailed
|
||||||
|
| Self::OutgoingWebhookEncodingFailed
|
||||||
|
| Self::OutgoingWebhookProcessTrackerTaskUpdateFailed
|
||||||
|
| Self::OutgoingWebhookRetrySchedulingFailed => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum ApplePayDecryptionError {
|
pub enum ApplePayDecryptionError {
|
||||||
#[error("Failed to base64 decode input data")]
|
#[error("Failed to base64 decode input data")]
|
||||||
|
|||||||
@ -659,8 +659,21 @@ pub(crate) async fn create_event_and_trigger_outgoing_webhook(
|
|||||||
primary_object_type: enums::EventObjectType,
|
primary_object_type: enums::EventObjectType,
|
||||||
content: api::OutgoingWebhookContent,
|
content: api::OutgoingWebhookContent,
|
||||||
) -> CustomResult<(), errors::ApiErrorResponse> {
|
) -> CustomResult<(), errors::ApiErrorResponse> {
|
||||||
let merchant_id = business_profile.merchant_id.clone();
|
|
||||||
let event_id = format!("{primary_object_id}_{event_type}");
|
let event_id = format!("{primary_object_id}_{event_type}");
|
||||||
|
|
||||||
|
if !state.conf.webhooks.outgoing_enabled
|
||||||
|
|| get_webhook_url_from_business_profile(&business_profile).is_err()
|
||||||
|
{
|
||||||
|
logger::debug!(
|
||||||
|
business_profile_id=%business_profile.profile_id,
|
||||||
|
%event_id,
|
||||||
|
"Outgoing webhooks are disabled in application configuration, or merchant webhook URL \
|
||||||
|
could not be obtained; skipping outgoing webhooks for event"
|
||||||
|
);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let merchant_id = business_profile.merchant_id.clone();
|
||||||
let new_event = storage::EventNew {
|
let new_event = storage::EventNew {
|
||||||
event_id: event_id.clone(),
|
event_id: event_id.clone(),
|
||||||
event_type,
|
event_type,
|
||||||
@ -688,7 +701,6 @@ pub(crate) async fn create_event_and_trigger_outgoing_webhook(
|
|||||||
}
|
}
|
||||||
}?;
|
}?;
|
||||||
|
|
||||||
if state.conf.webhooks.outgoing_enabled {
|
|
||||||
let outgoing_webhook = api::OutgoingWebhook {
|
let outgoing_webhook = api::OutgoingWebhook {
|
||||||
merchant_id: merchant_id.clone(),
|
merchant_id: merchant_id.clone(),
|
||||||
event_id: event.event_id.clone(),
|
event_id: event.event_id.clone(),
|
||||||
@ -731,7 +743,6 @@ pub(crate) async fn create_event_and_trigger_outgoing_webhook(
|
|||||||
}
|
}
|
||||||
.in_current_span(),
|
.in_current_span(),
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -817,21 +828,30 @@ async fn trigger_webhook_to_merchant<W: types::OutgoingWebhookType>(
|
|||||||
delivery_attempt: types::WebhookDeliveryAttempt,
|
delivery_attempt: types::WebhookDeliveryAttempt,
|
||||||
process_tracker: Option<storage::ProcessTracker>,
|
process_tracker: Option<storage::ProcessTracker>,
|
||||||
) -> CustomResult<(), errors::WebhooksFlowError> {
|
) -> CustomResult<(), errors::WebhooksFlowError> {
|
||||||
let webhook_details_json = business_profile
|
let webhook_url = match (
|
||||||
.webhook_details
|
get_webhook_url_from_business_profile(&business_profile),
|
||||||
.get_required_value("webhook_details")
|
process_tracker.clone(),
|
||||||
.change_context(errors::WebhooksFlowError::MerchantWebhookDetailsNotFound)?;
|
) {
|
||||||
|
(Ok(webhook_url), _) => Ok(webhook_url),
|
||||||
let webhook_details: api::WebhookDetails =
|
(Err(error), Some(process_tracker)) => {
|
||||||
webhook_details_json
|
if !error
|
||||||
.parse_value("WebhookDetails")
|
.current_context()
|
||||||
.change_context(errors::WebhooksFlowError::MerchantWebhookDetailsNotFound)?;
|
.is_webhook_delivery_retryable_error()
|
||||||
|
{
|
||||||
let webhook_url = webhook_details
|
logger::debug!("Failed to obtain merchant webhook URL, aborting retries");
|
||||||
.webhook_url
|
state
|
||||||
.get_required_value("webhook_url")
|
.store
|
||||||
.change_context(errors::WebhooksFlowError::MerchantWebhookURLNotConfigured)
|
.as_scheduler()
|
||||||
.map(ExposeInterface::expose)?;
|
.finish_process_with_business_status(process_tracker, "FAILURE".into())
|
||||||
|
.await
|
||||||
|
.change_context(
|
||||||
|
errors::WebhooksFlowError::OutgoingWebhookProcessTrackerTaskUpdateFailed,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
Err(error)
|
||||||
|
}
|
||||||
|
(Err(error), None) => Err(error),
|
||||||
|
}?;
|
||||||
|
|
||||||
let outgoing_webhook_event_id = webhook.event_id.clone();
|
let outgoing_webhook_event_id = webhook.event_id.clone();
|
||||||
|
|
||||||
@ -1579,7 +1599,7 @@ pub async fn add_outgoing_webhook_retry_task_to_process_tracker(
|
|||||||
let process_tracker_id = scheduler::utils::get_process_tracker_id(
|
let process_tracker_id = scheduler::utils::get_process_tracker_id(
|
||||||
runner,
|
runner,
|
||||||
task,
|
task,
|
||||||
&event.primary_object_id,
|
&event.event_id,
|
||||||
&business_profile.merchant_id,
|
&business_profile.merchant_id,
|
||||||
);
|
);
|
||||||
let process_tracker_entry = storage::ProcessTrackerNew::new(
|
let process_tracker_entry = storage::ProcessTrackerNew::new(
|
||||||
@ -1611,3 +1631,24 @@ pub async fn add_outgoing_webhook_retry_task_to_process_tracker(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_webhook_url_from_business_profile(
|
||||||
|
business_profile: &diesel_models::business_profile::BusinessProfile,
|
||||||
|
) -> CustomResult<String, errors::WebhooksFlowError> {
|
||||||
|
let webhook_details_json = business_profile
|
||||||
|
.webhook_details
|
||||||
|
.clone()
|
||||||
|
.get_required_value("webhook_details")
|
||||||
|
.change_context(errors::WebhooksFlowError::MerchantWebhookDetailsNotFound)?;
|
||||||
|
|
||||||
|
let webhook_details: api::WebhookDetails =
|
||||||
|
webhook_details_json
|
||||||
|
.parse_value("WebhookDetails")
|
||||||
|
.change_context(errors::WebhooksFlowError::MerchantWebhookDetailsNotFound)?;
|
||||||
|
|
||||||
|
webhook_details
|
||||||
|
.webhook_url
|
||||||
|
.get_required_value("webhook_url")
|
||||||
|
.change_context(errors::WebhooksFlowError::MerchantWebhookUrlNotConfigured)
|
||||||
|
.map(ExposeInterface::expose)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user