fix(router): handle JSON connector response parse error (#1892)

This commit is contained in:
AkshayaFoiger
2023-08-09 12:17:59 +05:30
committed by GitHub
parent f8ef52c645
commit 393c2ab94c
4 changed files with 123 additions and 89 deletions

View File

@ -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<types::ErrorResponse, errors::ConnectorError> {
let response: Result<braintree::ErrorResponse, Report<common_utils::errors::ParsingError>> =
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<types::ErrorResponse, errors::ConnectorError> {
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<types::ErrorResponse, errors::ConnectorError> {
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<types::ErrorResponse, errors::ConnectorError> {
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<types::ErrorResponse, errors::ConnectorError> {
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(

View File

@ -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<ErrorResponse, errors::ConnectorError> {
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<common_utils::errors::ParsingError>,
> = 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())
}
}
}
}

View File

@ -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<ErrorResponse, errors::ConnectorError> {
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::<Vec<String>>()
.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<common_utils::errors::ParsingError>,
> = 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::<Vec<String>>()
.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())
}
}
}
}

View File

@ -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<types::ErrorResponse, errors::ConnectorError> {
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::<Value>(&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),
})
}
}
}