mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-30 01:27:31 +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
|
/// 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
|
||||||
|
|||||||
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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]
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user