diff --git a/crates/common_utils/src/consts.rs b/crates/common_utils/src/consts.rs index 59ec537769..bbb65e1afc 100644 --- a/crates/common_utils/src/consts.rs +++ b/crates/common_utils/src/consts.rs @@ -97,6 +97,11 @@ pub const BASE64_ENGINE: base64::engine::GeneralPurpose = base64::engine::genera /// URL Safe base64 engine pub const BASE64_ENGINE_URL_SAFE: base64::engine::GeneralPurpose = base64::engine::general_purpose::URL_SAFE; + +/// URL Safe base64 engine without padding +pub const BASE64_ENGINE_URL_SAFE_NO_PAD: base64::engine::GeneralPurpose = + base64::engine::general_purpose::URL_SAFE_NO_PAD; + /// Regex for matching a domain /// Eg - /// http://www.example.com diff --git a/crates/router/src/core/errors.rs b/crates/router/src/core/errors.rs index bf5330a932..a980bb664e 100644 --- a/crates/router/src/core/errors.rs +++ b/crates/router/src/core/errors.rs @@ -157,6 +157,8 @@ pub enum WebhooksFlowError { OutgoingWebhookRetrySchedulingFailed, #[error("Outgoing webhook response encoding failed")] OutgoingWebhookResponseEncodingFailed, + #[error("ID generation failed")] + IdGenerationFailed, } impl WebhooksFlowError { @@ -174,7 +176,8 @@ impl WebhooksFlowError { | Self::DisputeWebhookValidationFailed | Self::OutgoingWebhookEncodingFailed | Self::OutgoingWebhookProcessTrackerTaskUpdateFailed - | Self::OutgoingWebhookRetrySchedulingFailed => true, + | Self::OutgoingWebhookRetrySchedulingFailed + | Self::IdGenerationFailed => true, } } } diff --git a/crates/router/src/core/webhooks/outgoing.rs b/crates/router/src/core/webhooks/outgoing.rs index 34b49c2fc8..adc54b09b9 100644 --- a/crates/router/src/core/webhooks/outgoing.rs +++ b/crates/router/src/core/webhooks/outgoing.rs @@ -60,7 +60,9 @@ pub(crate) async fn create_event_and_trigger_outgoing_webhook( ) -> CustomResult<(), errors::ApiErrorResponse> { let delivery_attempt = enums::WebhookDeliveryAttempt::InitialAttempt; let idempotent_event_id = - utils::get_idempotent_event_id(&primary_object_id, event_type, delivery_attempt); + utils::get_idempotent_event_id(&primary_object_id, event_type, delivery_attempt) + .change_context(errors::ApiErrorResponse::WebhookProcessingFailure) + .attach_printable("Failed to generate idempotent event ID")?; let webhook_url_result = get_webhook_url_from_business_profile(&business_profile); if !state.conf.webhooks.outgoing_enabled diff --git a/crates/router/src/core/webhooks/outgoing_v2.rs b/crates/router/src/core/webhooks/outgoing_v2.rs index 07758817e6..d0e0ecaa69 100644 --- a/crates/router/src/core/webhooks/outgoing_v2.rs +++ b/crates/router/src/core/webhooks/outgoing_v2.rs @@ -48,7 +48,9 @@ pub(crate) async fn create_event_and_trigger_outgoing_webhook( ) -> CustomResult<(), errors::ApiErrorResponse> { let delivery_attempt = enums::WebhookDeliveryAttempt::InitialAttempt; let idempotent_event_id = - utils::get_idempotent_event_id(&primary_object_id, event_type, delivery_attempt); + utils::get_idempotent_event_id(&primary_object_id, event_type, delivery_attempt) + .change_context(errors::ApiErrorResponse::WebhookProcessingFailure) + .attach_printable("Failed to generate idempotent event ID")?; let webhook_url_result = business_profile .get_webhook_url_from_profile() .change_context(errors::WebhooksFlowError::MerchantWebhookUrlNotConfigured); diff --git a/crates/router/src/core/webhooks/utils.rs b/crates/router/src/core/webhooks/utils.rs index 45b6c24a2e..0344d7d010 100644 --- a/crates/router/src/core/webhooks/utils.rs +++ b/crates/router/src/core/webhooks/utils.rs @@ -1,6 +1,12 @@ use std::marker::PhantomData; -use common_utils::{errors::CustomResult, ext_traits::ValueExt}; +use base64::Engine; +use common_utils::{ + consts, + crypto::{self, GenerateDigest}, + errors::CustomResult, + ext_traits::ValueExt, +}; use error_stack::{Report, ResultExt}; use redis_interface as redis; use router_env::tracing; @@ -146,18 +152,28 @@ pub(crate) fn get_idempotent_event_id( primary_object_id: &str, event_type: types::storage::enums::EventType, delivery_attempt: types::storage::enums::WebhookDeliveryAttempt, -) -> String { +) -> Result> { use crate::types::storage::enums::WebhookDeliveryAttempt; const EVENT_ID_SUFFIX_LENGTH: usize = 8; let common_prefix = format!("{primary_object_id}_{event_type}"); - match delivery_attempt { - WebhookDeliveryAttempt::InitialAttempt => common_prefix, + + // Hash the common prefix with SHA256 and encode with URL-safe base64 without padding + let digest = crypto::Sha256 + .generate_digest(common_prefix.as_bytes()) + .change_context(errors::WebhooksFlowError::IdGenerationFailed) + .attach_printable("Failed to generate idempotent event ID")?; + let base_encoded = consts::BASE64_ENGINE_URL_SAFE_NO_PAD.encode(digest); + + let result = match delivery_attempt { + WebhookDeliveryAttempt::InitialAttempt => base_encoded, WebhookDeliveryAttempt::AutomaticRetry | WebhookDeliveryAttempt::ManualRetry => { - common_utils::generate_id(EVENT_ID_SUFFIX_LENGTH, &common_prefix) + common_utils::generate_id(EVENT_ID_SUFFIX_LENGTH, &base_encoded) } - } + }; + + Ok(result) } #[inline] diff --git a/crates/router/src/core/webhooks/webhook_events.rs b/crates/router/src/core/webhooks/webhook_events.rs index 44ce4fdd51..700295af0e 100644 --- a/crates/router/src/core/webhooks/webhook_events.rs +++ b/crates/router/src/core/webhooks/webhook_events.rs @@ -287,7 +287,9 @@ pub async fn retry_delivery_attempt( &event_to_retry.primary_object_id, event_to_retry.event_type, delivery_attempt, - ); + ) + .change_context(errors::ApiErrorResponse::WebhookProcessingFailure) + .attach_printable("Failed to generate idempotent event ID")?; let now = common_utils::date_time::now(); let new_event = domain::Event { diff --git a/crates/router/src/workflows/outgoing_webhook_retry.rs b/crates/router/src/workflows/outgoing_webhook_retry.rs index 559f62b1a4..d84f0b26dc 100644 --- a/crates/router/src/workflows/outgoing_webhook_retry.rs +++ b/crates/router/src/workflows/outgoing_webhook_retry.rs @@ -72,7 +72,9 @@ impl ProcessTrackerWorkflow for OutgoingWebhookRetryWorkflow { &tracking_data.primary_object_id, tracking_data.event_type, delivery_attempt, - ); + ) + .change_context(errors::ApiErrorResponse::WebhookProcessingFailure) + .attach_printable("Failed to generate idempotent event ID")?; let initial_event = match &tracking_data.initial_attempt_id { Some(initial_attempt_id) => {