diff --git a/crates/router/src/connector/braintree.rs b/crates/router/src/connector/braintree.rs index 745fc4fbc5..4184408619 100644 --- a/crates/router/src/connector/braintree.rs +++ b/crates/router/src/connector/braintree.rs @@ -2,7 +2,7 @@ pub mod transformers; use std::fmt::Debug; -use error_stack::{IntoReport, ResultExt}; +use error_stack::{IntoReport, Report, ResultExt}; use masking::PeekInterface; use self::transformers as braintree; @@ -10,7 +10,7 @@ use crate::{ configs::settings, consts, core::errors::{self, CustomResult}, - headers, + headers, logger, services::{ self, request::{self, Mask}, @@ -46,6 +46,27 @@ impl ConnectorCommon for Braintree { auth.auth_header.into_masked(), )]) } + + fn build_error_response( + &self, + res: types::Response, + ) -> CustomResult { + let response: Result> = + res.response.parse_struct("Braintree Error Response"); + + match response { + Ok(response_data) => Ok(types::ErrorResponse { + status_code: res.status_code, + code: consts::NO_ERROR_CODE.to_string(), + message: response_data.api_error_response.message, + reason: None, + }), + Err(error_msg) => { + logger::error!(deserialization_error =? error_msg); + utils::handle_json_response_deserialization_failure(res, "braintree".to_owned()) + } + } + } } impl api::Payment for Braintree {} @@ -140,17 +161,7 @@ impl &self, res: types::Response, ) -> CustomResult { - let response: braintree::ErrorResponse = res - .response - .parse_struct("Error Response") - .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - - Ok(types::ErrorResponse { - status_code: res.status_code, - code: consts::NO_ERROR_CODE.to_string(), - message: response.api_error_response.message, - reason: None, - }) + self.build_error_response(res) } fn get_request_body( @@ -291,17 +302,7 @@ impl &self, res: types::Response, ) -> CustomResult { - let response: braintree::ErrorResponse = res - .response - .parse_struct("Braintree Error Response") - .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - - Ok(types::ErrorResponse { - status_code: res.status_code, - code: consts::NO_ERROR_CODE.to_string(), - message: response.api_error_response.message, - reason: None, - }) + self.build_error_response(res) } fn get_request_body( @@ -429,16 +430,7 @@ impl &self, res: types::Response, ) -> CustomResult { - let response: braintree::ErrorResponse = res - .response - .parse_struct("Braintree ErrorResponse") - .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - Ok(types::ErrorResponse { - status_code: res.status_code, - code: consts::NO_ERROR_CODE.to_string(), - message: response.api_error_response.message, - reason: None, - }) + self.build_error_response(res) } } @@ -511,17 +503,7 @@ impl &self, res: types::Response, ) -> CustomResult { - let response: braintree::ErrorResponse = res - .response - .parse_struct("Braintree ErrorResponse") - .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - - Ok(types::ErrorResponse { - status_code: res.status_code, - code: consts::NO_ERROR_CODE.to_string(), - message: response.api_error_response.message, - reason: None, - }) + self.build_error_response(res) } fn get_request_body( diff --git a/crates/router/src/connector/rapyd.rs b/crates/router/src/connector/rapyd.rs index 02cc153da4..960b3bbfc9 100644 --- a/crates/router/src/connector/rapyd.rs +++ b/crates/router/src/connector/rapyd.rs @@ -3,7 +3,7 @@ use std::fmt::Debug; use base64::Engine; use common_utils::{date_time, ext_traits::StringExt}; -use error_stack::{IntoReport, ResultExt}; +use error_stack::{IntoReport, Report, ResultExt}; use masking::{ExposeInterface, PeekInterface}; use rand::distributions::{Alphanumeric, DistString}; use ring::hmac; @@ -15,7 +15,7 @@ use crate::{ consts, core::errors::{self, CustomResult}, db::StorageInterface, - headers, + headers, logger, services::{ self, request::{self, Mask}, @@ -82,16 +82,23 @@ impl ConnectorCommon for Rapyd { &self, res: types::Response, ) -> CustomResult { - let response: rapyd::RapydPaymentsResponse = res - .response - .parse_struct("Rapyd ErrorResponse") - .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - Ok(ErrorResponse { - status_code: res.status_code, - code: response.status.error_code, - message: response.status.status.unwrap_or_default(), - reason: response.status.message, - }) + let response: Result< + rapyd::RapydPaymentsResponse, + Report, + > = res.response.parse_struct("Rapyd ErrorResponse"); + + match response { + Ok(response_data) => Ok(ErrorResponse { + status_code: res.status_code, + code: response_data.status.error_code, + message: response_data.status.status.unwrap_or_default(), + reason: response_data.status.message, + }), + Err(error_msg) => { + logger::error!(deserialization_error =? error_msg); + utils::handle_json_response_deserialization_failure(res, "rapyd".to_owned()) + } + } } } diff --git a/crates/router/src/connector/trustpay.rs b/crates/router/src/connector/trustpay.rs index 14155065a6..14dbe14f4d 100644 --- a/crates/router/src/connector/trustpay.rs +++ b/crates/router/src/connector/trustpay.rs @@ -4,7 +4,7 @@ use std::fmt::Debug; use base64::Engine; use common_utils::{crypto, errors::ReportSwitchExt, ext_traits::ByteSliceExt}; -use error_stack::{IntoReport, ResultExt}; +use error_stack::{IntoReport, Report, ResultExt}; use masking::PeekInterface; use transformers as trustpay; @@ -19,7 +19,7 @@ use crate::{ errors::{self, CustomResult}, payments, }, - headers, + headers, logger, services::{ self, request::{self, Mask}, @@ -104,34 +104,43 @@ impl ConnectorCommon for Trustpay { &self, res: Response, ) -> CustomResult { - let response: trustpay::TrustpayErrorResponse = res - .response - .parse_struct("trustpay ErrorResponse") - .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - let error_list = response.errors.clone().unwrap_or(vec![]); - let option_error_code_message = get_error_code_error_message_based_on_priority( - self.clone(), - error_list.into_iter().map(|errors| errors.into()).collect(), - ); - let reason = response.errors.map(|errors| { - errors - .iter() - .map(|error| error.description.clone()) - .collect::>() - .join(" & ") - }); - Ok(ErrorResponse { - status_code: res.status_code, - code: option_error_code_message - .clone() - .map(|error_code_message| error_code_message.error_code) - .unwrap_or(consts::NO_ERROR_CODE.to_string()), - // message vary for the same code, so relying on code alone as it is unique - message: option_error_code_message - .map(|error_code_message| error_code_message.error_code) - .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), - reason: reason.or(response.description), - }) + let response: Result< + trustpay::TrustpayErrorResponse, + Report, + > = res.response.parse_struct("trustpay ErrorResponse"); + + match response { + Ok(response_data) => { + let error_list = response_data.errors.clone().unwrap_or(vec![]); + let option_error_code_message = get_error_code_error_message_based_on_priority( + self.clone(), + error_list.into_iter().map(|errors| errors.into()).collect(), + ); + let reason = response_data.errors.map(|errors| { + errors + .iter() + .map(|error| error.description.clone()) + .collect::>() + .join(" & ") + }); + Ok(ErrorResponse { + status_code: res.status_code, + code: option_error_code_message + .clone() + .map(|error_code_message| error_code_message.error_code) + .unwrap_or(consts::NO_ERROR_CODE.to_string()), + // message vary for the same code, so relying on code alone as it is unique + message: option_error_code_message + .map(|error_code_message| error_code_message.error_code) + .unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), + reason: reason.or(response_data.description), + }) + } + Err(error_msg) => { + logger::error!(deserialization_error =? error_msg); + utils::handle_json_response_deserialization_failure(res, "trustpay".to_owned()) + } + } } } diff --git a/crates/router/src/utils.rs b/crates/router/src/utils.rs index bf943c09ef..1c9d6f4300 100644 --- a/crates/router/src/utils.rs +++ b/crates/router/src/utils.rs @@ -17,13 +17,16 @@ use image::Luma; use nanoid::nanoid; use qrcode; use serde::de::DeserializeOwned; +use serde_json::Value; use uuid::Uuid; pub use self::ext_traits::{OptionExt, ValidateCall}; use crate::{ consts, - core::errors::{self, RouterResult}, - logger, types, + core::errors::{self, CustomResult, RouterResult}, + logger, + routes::metrics, + types, }; pub mod error_parser { @@ -176,3 +179,36 @@ mod tests { assert!(qr_image_data_source_url.is_ok()); } } + +// validate json format for the error +pub fn handle_json_response_deserialization_failure( + res: types::Response, + connector: String, +) -> CustomResult { + metrics::RESPONSE_DESERIALIZATION_FAILURE.add( + &metrics::CONTEXT, + 1, + &[metrics::request::add_attributes("connector", connector)], + ); + + let response_data = String::from_utf8(res.response.to_vec()) + .into_report() + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + + // check for whether the response is in json format + match serde_json::from_str::(&response_data) { + // in case of unexpected response but in json format + Ok(_) => Err(errors::ConnectorError::ResponseDeserializationFailed)?, + // in case of unexpected response but in html or string format + Err(error_msg) => { + logger::error!(deserialization_error=?error_msg); + logger::error!("UNEXPECTED RESPONSE FROM CONNECTOR: {}", response_data); + Ok(types::ErrorResponse { + status_code: res.status_code, + code: consts::NO_ERROR_CODE.to_string(), + message: consts::UNSUPPORTED_ERROR_MESSAGE.to_string(), + reason: Some(response_data), + }) + } + } +}