fix(webhooks): add idempotent_event_id generation using URL-safe Base64 (no padding) and SHA256 digest (#9405)

Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
Ayush Anand
2025-09-22 13:15:52 +05:30
committed by GitHub
parent 740f3af643
commit 0c38bc3d0c
7 changed files with 43 additions and 11 deletions

View File

@ -97,6 +97,11 @@ pub const BASE64_ENGINE: base64::engine::GeneralPurpose = base64::engine::genera
/// URL Safe base64 engine /// URL Safe base64 engine
pub const BASE64_ENGINE_URL_SAFE: base64::engine::GeneralPurpose = pub const BASE64_ENGINE_URL_SAFE: base64::engine::GeneralPurpose =
base64::engine::general_purpose::URL_SAFE; 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 /// Regex for matching a domain
/// Eg - /// Eg -
/// http://www.example.com /// http://www.example.com

View File

@ -157,6 +157,8 @@ pub enum WebhooksFlowError {
OutgoingWebhookRetrySchedulingFailed, OutgoingWebhookRetrySchedulingFailed,
#[error("Outgoing webhook response encoding failed")] #[error("Outgoing webhook response encoding failed")]
OutgoingWebhookResponseEncodingFailed, OutgoingWebhookResponseEncodingFailed,
#[error("ID generation failed")]
IdGenerationFailed,
} }
impl WebhooksFlowError { impl WebhooksFlowError {
@ -174,7 +176,8 @@ impl WebhooksFlowError {
| Self::DisputeWebhookValidationFailed | Self::DisputeWebhookValidationFailed
| Self::OutgoingWebhookEncodingFailed | Self::OutgoingWebhookEncodingFailed
| Self::OutgoingWebhookProcessTrackerTaskUpdateFailed | Self::OutgoingWebhookProcessTrackerTaskUpdateFailed
| Self::OutgoingWebhookRetrySchedulingFailed => true, | Self::OutgoingWebhookRetrySchedulingFailed
| Self::IdGenerationFailed => true,
} }
} }
} }

View File

@ -60,7 +60,9 @@ pub(crate) async fn create_event_and_trigger_outgoing_webhook(
) -> CustomResult<(), errors::ApiErrorResponse> { ) -> CustomResult<(), errors::ApiErrorResponse> {
let delivery_attempt = enums::WebhookDeliveryAttempt::InitialAttempt; let delivery_attempt = enums::WebhookDeliveryAttempt::InitialAttempt;
let idempotent_event_id = 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); let webhook_url_result = get_webhook_url_from_business_profile(&business_profile);
if !state.conf.webhooks.outgoing_enabled if !state.conf.webhooks.outgoing_enabled

View File

@ -48,7 +48,9 @@ pub(crate) async fn create_event_and_trigger_outgoing_webhook(
) -> CustomResult<(), errors::ApiErrorResponse> { ) -> CustomResult<(), errors::ApiErrorResponse> {
let delivery_attempt = enums::WebhookDeliveryAttempt::InitialAttempt; let delivery_attempt = enums::WebhookDeliveryAttempt::InitialAttempt;
let idempotent_event_id = 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 let webhook_url_result = business_profile
.get_webhook_url_from_profile() .get_webhook_url_from_profile()
.change_context(errors::WebhooksFlowError::MerchantWebhookUrlNotConfigured); .change_context(errors::WebhooksFlowError::MerchantWebhookUrlNotConfigured);

View File

@ -1,6 +1,12 @@
use std::marker::PhantomData; 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 error_stack::{Report, ResultExt};
use redis_interface as redis; use redis_interface as redis;
use router_env::tracing; use router_env::tracing;
@ -146,18 +152,28 @@ pub(crate) fn get_idempotent_event_id(
primary_object_id: &str, primary_object_id: &str,
event_type: types::storage::enums::EventType, event_type: types::storage::enums::EventType,
delivery_attempt: types::storage::enums::WebhookDeliveryAttempt, delivery_attempt: types::storage::enums::WebhookDeliveryAttempt,
) -> String { ) -> Result<String, Report<errors::WebhooksFlowError>> {
use crate::types::storage::enums::WebhookDeliveryAttempt; use crate::types::storage::enums::WebhookDeliveryAttempt;
const EVENT_ID_SUFFIX_LENGTH: usize = 8; const EVENT_ID_SUFFIX_LENGTH: usize = 8;
let common_prefix = format!("{primary_object_id}_{event_type}"); 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 => { 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] #[inline]

View File

@ -287,7 +287,9 @@ pub async fn retry_delivery_attempt(
&event_to_retry.primary_object_id, &event_to_retry.primary_object_id,
event_to_retry.event_type, event_to_retry.event_type,
delivery_attempt, delivery_attempt,
); )
.change_context(errors::ApiErrorResponse::WebhookProcessingFailure)
.attach_printable("Failed to generate idempotent event ID")?;
let now = common_utils::date_time::now(); let now = common_utils::date_time::now();
let new_event = domain::Event { let new_event = domain::Event {

View File

@ -72,7 +72,9 @@ impl ProcessTrackerWorkflow<SessionState> for OutgoingWebhookRetryWorkflow {
&tracking_data.primary_object_id, &tracking_data.primary_object_id,
tracking_data.event_type, tracking_data.event_type,
delivery_attempt, delivery_attempt,
); )
.change_context(errors::ApiErrorResponse::WebhookProcessingFailure)
.attach_printable("Failed to generate idempotent event ID")?;
let initial_event = match &tracking_data.initial_attempt_id { let initial_event = match &tracking_data.initial_attempt_id {
Some(initial_attempt_id) => { Some(initial_attempt_id) => {