mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-29 09:07:09 +08:00
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:
@ -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
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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<String, Report<errors::WebhooksFlowError>> {
|
||||
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]
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -72,7 +72,9 @@ impl ProcessTrackerWorkflow<SessionState> 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) => {
|
||||
|
||||
Reference in New Issue
Block a user