pub mod client; pub mod request; use std::{ collections::HashMap, error::Error, fmt::Debug, future::Future, str, time::{Duration, Instant}, }; use actix_web::{body, web, FromRequest, HttpRequest, HttpResponse, Responder, ResponseError}; use api_models::enums::CaptureMethod; pub use client::{proxy_bypass_urls, ApiClient, MockApiClient, ProxyClient}; use common_enums::Currency; pub use common_utils::request::{ContentType, Method, Request, RequestBuilder}; use common_utils::{ consts::X_HS_LATENCY, errors::{ErrorSwitch, ReportSwitchExt}, request::RequestContent, }; use error_stack::{report, IntoReport, Report, ResultExt}; use masking::{PeekInterface, Secret}; use router_env::{instrument, tracing, tracing_actix_web::RequestId, Tag}; use serde::Serialize; use serde_json::json; use tera::{Context, Tera}; use self::request::{HeaderExt, RequestBuilderExt}; use super::authentication::AuthenticateAndFetch; use crate::{ configs::settings::{Connectors, Settings}, consts, core::{ api_locking, errors::{self, CustomResult}, payments, }, events::{ api_logs::{ApiEvent, ApiEventMetric, ApiEventsType}, connector_api_logs::ConnectorEvent, }, logger, routes::{ app::AppStateInfo, metrics::{self, request as metrics_request}, AppState, }, types::{ self, api::{self, ConnectorCommon}, ErrorResponse, }, }; pub type BoxedConnectorIntegration<'a, T, Req, Resp> = Box<&'a (dyn ConnectorIntegration + Send + Sync)>; pub trait ConnectorIntegrationAny: Send + Sync + 'static { fn get_connector_integration(&self) -> BoxedConnectorIntegration<'_, T, Req, Resp>; } impl ConnectorIntegrationAny for S where S: ConnectorIntegration + Send + Sync, { fn get_connector_integration(&self) -> BoxedConnectorIntegration<'_, T, Req, Resp> { Box::new(self) } } pub trait ConnectorValidation: ConnectorCommon { fn validate_capture_method( &self, capture_method: Option, ) -> CustomResult<(), errors::ConnectorError> { let capture_method = capture_method.unwrap_or_default(); match capture_method { CaptureMethod::Automatic => Ok(()), CaptureMethod::Manual | CaptureMethod::ManualMultiple | CaptureMethod::Scheduled => { Err(errors::ConnectorError::NotSupported { message: capture_method.to_string(), connector: self.id(), } .into()) } } } fn validate_psync_reference_id( &self, data: &types::PaymentsSyncRouterData, ) -> CustomResult<(), errors::ConnectorError> { data.request .connector_transaction_id .get_connector_transaction_id() .change_context(errors::ConnectorError::MissingConnectorTransactionID) .map(|_| ()) } fn is_webhook_source_verification_mandatory(&self) -> bool { false } } #[async_trait::async_trait] pub trait ConnectorIntegration: ConnectorIntegrationAny + Sync { fn get_headers( &self, _req: &types::RouterData, _connectors: &Connectors, ) -> CustomResult)>, errors::ConnectorError> { Ok(vec![]) } fn get_content_type(&self) -> &'static str { mime::APPLICATION_JSON.essence_str() } /// primarily used when creating signature based on request method of payment flow fn get_http_method(&self) -> Method { Method::Post } fn get_url( &self, _req: &types::RouterData, _connectors: &Connectors, ) -> CustomResult { Ok(String::new()) } fn get_request_body( &self, _req: &types::RouterData, _connectors: &Connectors, ) -> CustomResult { Ok(RequestContent::Json(Box::new(json!(r#"{}"#)))) } fn get_request_form_data( &self, _req: &types::RouterData, ) -> CustomResult, errors::ConnectorError> { Ok(None) } /// This module can be called before executing a payment flow where a pre-task is needed /// Eg: Some connectors requires one-time session token before making a payment, we can add the session token creation logic in this block async fn execute_pretasks( &self, _router_data: &mut types::RouterData, _app_state: &AppState, ) -> CustomResult<(), errors::ConnectorError> { Ok(()) } /// This module can be called after executing a payment flow where a post-task needed /// Eg: Some connectors require payment sync to happen immediately after the authorize call to complete the transaction, we can add that logic in this block async fn execute_posttasks( &self, _router_data: &mut types::RouterData, _app_state: &AppState, ) -> CustomResult<(), errors::ConnectorError> { Ok(()) } fn build_request( &self, req: &types::RouterData, _connectors: &Connectors, ) -> CustomResult, errors::ConnectorError> { metrics::UNIMPLEMENTED_FLOW.add( &metrics::CONTEXT, 1, &[metrics::request::add_attributes( "connector", req.connector.clone(), )], ); Ok(None) } fn handle_response( &self, data: &types::RouterData, _res: types::Response, ) -> CustomResult, errors::ConnectorError> where T: Clone, Req: Clone, Resp: Clone, { Ok(data.clone()) } fn get_error_response( &self, _res: types::Response, ) -> CustomResult { Ok(ErrorResponse::get_not_implemented()) } fn get_5xx_error_response( &self, res: types::Response, ) -> CustomResult { let error_message = match res.status_code { 500 => "internal_server_error", 501 => "not_implemented", 502 => "bad_gateway", 503 => "service_unavailable", 504 => "gateway_timeout", 505 => "http_version_not_supported", 506 => "variant_also_negotiates", 507 => "insufficient_storage", 508 => "loop_detected", 510 => "not_extended", 511 => "network_authentication_required", _ => "unknown_error", }; Ok(ErrorResponse { code: res.status_code.to_string(), message: error_message.to_string(), reason: String::from_utf8(res.response.to_vec()).ok(), status_code: res.status_code, attempt_status: None, connector_transaction_id: None, }) } // whenever capture sync is implemented at the connector side, this method should be overridden fn get_multiple_capture_sync_method( &self, ) -> CustomResult { Err(errors::ConnectorError::NotImplemented("multiple capture sync".into()).into()) } fn get_certificate( &self, _req: &types::RouterData, ) -> CustomResult, errors::ConnectorError> { Ok(None) } fn get_certificate_key( &self, _req: &types::RouterData, ) -> CustomResult, errors::ConnectorError> { Ok(None) } } pub enum CaptureSyncMethod { Individual, Bulk, } /// Handle the flow by interacting with connector module /// `connector_request` is applicable only in case if the `CallConnectorAction` is `Trigger` /// In other cases, It will be created if required, even if it is not passed #[instrument(skip_all)] pub async fn execute_connector_processing_step< 'b, 'a, T: 'static, Req: Debug + Clone + 'static, Resp: Debug + Clone + 'static, >( state: &'b AppState, connector_integration: BoxedConnectorIntegration<'a, T, Req, Resp>, req: &'b types::RouterData, call_connector_action: payments::CallConnectorAction, connector_request: Option, ) -> CustomResult, errors::ConnectorError> where T: Clone + Debug, // BoxedConnectorIntegration: 'b, { // If needed add an error stack as follows // connector_integration.build_request(req).attach_printable("Failed to build request"); logger::debug!(connector_request=?connector_request); let mut router_data = req.clone(); match call_connector_action { payments::CallConnectorAction::HandleResponse(res) => { let response = types::Response { headers: None, response: res.into(), status_code: 200, }; connector_integration.handle_response(req, response) } payments::CallConnectorAction::Avoid => Ok(router_data), payments::CallConnectorAction::StatusUpdate { status, error_code, error_message, } => { router_data.status = status; let error_response = if error_code.is_some() | error_message.is_some() { Some(ErrorResponse { code: error_code.unwrap_or(consts::NO_ERROR_CODE.to_string()), message: error_message.unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), status_code: 200, // This status code is ignored in redirection response it will override with 302 status code. reason: None, attempt_status: None, connector_transaction_id: None, }) } else { None }; router_data.response = error_response.map(Err).unwrap_or(router_data.response); Ok(router_data) } payments::CallConnectorAction::Trigger => { metrics::CONNECTOR_CALL_COUNT.add( &metrics::CONTEXT, 1, &[ metrics::request::add_attributes("connector", req.connector.to_string()), metrics::request::add_attributes( "flow", std::any::type_name::() .split("::") .last() .unwrap_or_default() .to_string(), ), ], ); let connector_request = connector_request.or(connector_integration .build_request(req, &state.conf.connectors) .map_err(|error| { if matches!( error.current_context(), &errors::ConnectorError::RequestEncodingFailed | &errors::ConnectorError::RequestEncodingFailedWithReason(_) ) { metrics::REQUEST_BUILD_FAILURE.add( &metrics::CONTEXT, 1, &[metrics::request::add_attributes( "connector", req.connector.to_string(), )], ) } error })?); match connector_request { Some(request) => { logger::debug!(connector_request=?request); let masked_request_body = match &request.body { Some(request) => match request { RequestContent::Json(i) | RequestContent::FormUrlEncoded(i) | RequestContent::Xml(i) => i .masked_serialize() .unwrap_or(json!({ "error": "failed to mask serialize"})), RequestContent::FormData(_) => json!({"request_type": "FORM_DATA"}), }, None => serde_json::Value::Null, }; let request_url = request.url.clone(); let request_method = request.method; let current_time = Instant::now(); let response = call_connector_api(state, request).await; let external_latency = current_time.elapsed().as_millis(); logger::debug!(connector_response=?response); let status_code = response .as_ref() .map(|i| { i.as_ref() .map_or_else(|value| value.status_code, |value| value.status_code) }) .unwrap_or_default(); let connector_event = ConnectorEvent::new( req.connector.clone(), std::any::type_name::(), masked_request_body, response .as_ref() .map(|response| { response .as_ref() .map_or_else(|value| value, |value| value) .response .escape_ascii() .to_string() }) .ok(), request_url, request_method, req.payment_id.clone(), req.merchant_id.clone(), state.request_id.as_ref(), external_latency, req.refund_id.clone(), req.dispute_id.clone(), status_code, ); match connector_event.try_into() { Ok(event) => { state.event_handler().log_event(event); } Err(err) => { logger::error!(error=?err, "Error Logging Connector Event"); } } match response { Ok(body) => { let response = match body { Ok(body) => { let connector_http_status_code = Some(body.status_code); let mut data = connector_integration .handle_response(req, body) .map_err(|error| { if error.current_context() == &errors::ConnectorError::ResponseDeserializationFailed { metrics::RESPONSE_DESERIALIZATION_FAILURE.add( &metrics::CONTEXT, 1, &[metrics::request::add_attributes( "connector", req.connector.to_string(), )], ) } error })?; data.connector_http_status_code = connector_http_status_code; // Add up multiple external latencies in case of multiple external calls within the same request. data.external_latency = Some( data.external_latency .map_or(external_latency, |val| val + external_latency), ); data } Err(body) => { router_data.connector_http_status_code = Some(body.status_code); router_data.external_latency = Some( router_data .external_latency .map_or(external_latency, |val| val + external_latency), ); metrics::CONNECTOR_ERROR_RESPONSE_COUNT.add( &metrics::CONTEXT, 1, &[metrics::request::add_attributes( "connector", req.connector.clone(), )], ); let error = match body.status_code { 500..=511 => { connector_integration.get_5xx_error_response(body)? } _ => { let error_res = connector_integration.get_error_response(body)?; if let Some(status) = error_res.attempt_status { router_data.status = status; }; error_res } }; router_data.response = Err(error); router_data } }; Ok(response) } Err(error) => { if error.current_context().is_upstream_timeout() { let error_response = ErrorResponse { code: consts::REQUEST_TIMEOUT_ERROR_CODE.to_string(), message: consts::REQUEST_TIMEOUT_ERROR_MESSAGE.to_string(), reason: Some(consts::REQUEST_TIMEOUT_ERROR_MESSAGE.to_string()), status_code: 504, attempt_status: None, connector_transaction_id: None, }; router_data.response = Err(error_response); router_data.connector_http_status_code = Some(504); router_data.external_latency = Some( router_data .external_latency .map_or(external_latency, |val| val + external_latency), ); Ok(router_data) } else { Err(error.change_context( errors::ConnectorError::ProcessingStepFailed(None), )) } } } } None => Ok(router_data), } } } } #[instrument(skip_all)] pub async fn call_connector_api( state: &AppState, request: Request, ) -> CustomResult, errors::ApiClientError> { let current_time = Instant::now(); let response = state .api_client .send_request(state, request, None, true) .await; let elapsed_time = current_time.elapsed(); logger::info!(request_time=?elapsed_time); handle_response(response).await } #[instrument(skip_all)] pub async fn send_request( state: &AppState, request: Request, option_timeout_secs: Option, ) -> CustomResult { logger::debug!(method=?request.method, headers=?request.headers, payload=?request.body, ?request); let url = reqwest::Url::parse(&request.url) .into_report() .change_context(errors::ApiClientError::UrlEncodingFailed)?; #[cfg(feature = "dummy_connector")] let should_bypass_proxy = url .as_str() .starts_with(&state.conf.connectors.dummyconnector.base_url) || proxy_bypass_urls(&state.conf.locker).contains(&url.to_string()); #[cfg(not(feature = "dummy_connector"))] let should_bypass_proxy = proxy_bypass_urls(&state.conf.locker).contains(&url.to_string()); let client = client::create_client( &state.conf.proxy, should_bypass_proxy, request.certificate, request.certificate_key, )?; let headers = request.headers.construct_header_map()?; let metrics_tag = router_env::opentelemetry::KeyValue { key: consts::METRICS_HOST_TAG_NAME.into(), value: url.host_str().unwrap_or_default().to_string().into(), }; let request = { match request.method { Method::Get => client.get(url), Method::Post => { let client = client.post(url); match request.body { Some(RequestContent::Json(payload)) => client.json(&payload), Some(RequestContent::FormData(form)) => client.multipart(form), Some(RequestContent::FormUrlEncoded(payload)) => client.form(&payload), Some(RequestContent::Xml(payload)) => { let body = quick_xml::se::to_string(&payload) .into_report() .change_context(errors::ApiClientError::BodySerializationFailed)?; client.body(body).header("Content-Type", "application/xml") } None => client, } } Method::Put => { let client = client.put(url); match request.body { Some(RequestContent::Json(payload)) => client.json(&payload), Some(RequestContent::FormData(form)) => client.multipart(form), Some(RequestContent::FormUrlEncoded(payload)) => client.form(&payload), Some(RequestContent::Xml(payload)) => { let body = quick_xml::se::to_string(&payload) .into_report() .change_context(errors::ApiClientError::BodySerializationFailed)?; client.body(body).header("Content-Type", "application/xml") } None => client, } } Method::Patch => { let client = client.patch(url); match request.body { Some(RequestContent::Json(payload)) => client.json(&payload), Some(RequestContent::FormData(form)) => client.multipart(form), Some(RequestContent::FormUrlEncoded(payload)) => client.form(&payload), Some(RequestContent::Xml(payload)) => { let body = quick_xml::se::to_string(&payload) .into_report() .change_context(errors::ApiClientError::BodySerializationFailed)?; client.body(body).header("Content-Type", "application/xml") } None => client, } } Method::Delete => client.delete(url), } .add_headers(headers) .timeout(Duration::from_secs( option_timeout_secs.unwrap_or(crate::consts::REQUEST_TIME_OUT), )) }; // We cannot clone the request type, because it has Form trait which is not clonable. So we are cloning the request builder here. let cloned_send_request = request.try_clone().map(|cloned_request| async { cloned_request .send() .await .map_err(|error| match error { error if error.is_timeout() => { metrics::REQUEST_BUILD_FAILURE.add(&metrics::CONTEXT, 1, &[]); errors::ApiClientError::RequestTimeoutReceived } error if is_connection_closed_before_message_could_complete(&error) => { metrics::REQUEST_BUILD_FAILURE.add(&metrics::CONTEXT, 1, &[]); errors::ApiClientError::ConnectionClosedIncompleteMessage } _ => errors::ApiClientError::RequestNotSent(error.to_string()), }) .into_report() .attach_printable("Unable to send request to connector") }); let send_request = async { request .send() .await .map_err(|error| match error { error if error.is_timeout() => { metrics::REQUEST_BUILD_FAILURE.add(&metrics::CONTEXT, 1, &[]); errors::ApiClientError::RequestTimeoutReceived } error if is_connection_closed_before_message_could_complete(&error) => { metrics::REQUEST_BUILD_FAILURE.add(&metrics::CONTEXT, 1, &[]); errors::ApiClientError::ConnectionClosedIncompleteMessage } _ => errors::ApiClientError::RequestNotSent(error.to_string()), }) .into_report() .attach_printable("Unable to send request to connector") }; let response = metrics_request::record_operation_time( send_request, &metrics::EXTERNAL_REQUEST_TIME, &[metrics_tag.clone()], ) .await; // Retry once if the response is connection closed. // // This is just due to the racy nature of networking. // hyper has a connection pool of idle connections, and it selected one to send your request. // Most of the time, hyper will receive the server’s FIN and drop the dead connection from its pool. // But occasionally, a connection will be selected from the pool // and written to at the same time the server is deciding to close the connection. // Since hyper already wrote some of the request, // it can’t really retry it automatically on a new connection, since the server may have acted already match response { Ok(response) => Ok(response), Err(error) if error.current_context() == &errors::ApiClientError::ConnectionClosedIncompleteMessage => { metrics::AUTO_RETRY_CONNECTION_CLOSED.add(&metrics::CONTEXT, 1, &[]); match cloned_send_request { Some(cloned_request) => { logger::info!( "Retrying request due to connection closed before message could complete" ); metrics_request::record_operation_time( cloned_request, &metrics::EXTERNAL_REQUEST_TIME, &[metrics_tag], ) .await } None => { logger::info!("Retrying request due to connection closed before message could complete failed as request is not clonable"); Err(error) } } } err @ Err(_) => err, } } fn is_connection_closed_before_message_could_complete(error: &reqwest::Error) -> bool { let mut source = error.source(); while let Some(err) = source { if let Some(hyper_err) = err.downcast_ref::() { if hyper_err.is_incomplete_message() { return true; } } source = err.source(); } false } #[instrument(skip_all)] async fn handle_response( response: CustomResult, ) -> CustomResult, errors::ApiClientError> { response .map(|response| async { logger::info!(?response); let status_code = response.status().as_u16(); let headers = Some(response.headers().to_owned()); match status_code { 200..=202 | 302 | 204 => { logger::debug!(response=?response); // If needed add log line // logger:: error!( error_parsing_response=?err); let response = response .bytes() .await .into_report() .change_context(errors::ApiClientError::ResponseDecodingFailed) .attach_printable("Error while waiting for response")?; Ok(Ok(types::Response { headers, response, status_code, })) } status_code @ 500..=599 => { let bytes = response.bytes().await.map_err(|error| { report!(error) .change_context(errors::ApiClientError::ResponseDecodingFailed) .attach_printable("Client error response received") })?; // let error = match status_code { // 500 => errors::ApiClientError::InternalServerErrorReceived, // 502 => errors::ApiClientError::BadGatewayReceived, // 503 => errors::ApiClientError::ServiceUnavailableReceived, // 504 => errors::ApiClientError::GatewayTimeoutReceived, // _ => errors::ApiClientError::UnexpectedServerResponse, // }; Ok(Err(types::Response { headers, response: bytes, status_code, })) } status_code @ 400..=499 => { let bytes = response.bytes().await.map_err(|error| { report!(error) .change_context(errors::ApiClientError::ResponseDecodingFailed) .attach_printable("Client error response received") })?; /* let error = match status_code { 400 => errors::ApiClientError::BadRequestReceived(bytes), 401 => errors::ApiClientError::UnauthorizedReceived(bytes), 403 => errors::ApiClientError::ForbiddenReceived, 404 => errors::ApiClientError::NotFoundReceived(bytes), 405 => errors::ApiClientError::MethodNotAllowedReceived, 408 => errors::ApiClientError::RequestTimeoutReceived, 422 => errors::ApiClientError::UnprocessableEntityReceived(bytes), 429 => errors::ApiClientError::TooManyRequestsReceived, _ => errors::ApiClientError::UnexpectedServerResponse, }; Err(report!(error).attach_printable("Client error response received")) */ Ok(Err(types::Response { headers, response: bytes, status_code, })) } _ => Err(report!(errors::ApiClientError::UnexpectedServerResponse) .attach_printable("Unexpected response from server")), } })? .await } #[derive(Debug, Eq, PartialEq)] pub enum ApplicationResponse { Json(R), StatusOk, TextPlain(String), JsonForRedirection(api::RedirectionResponse), Form(Box), PaymenkLinkForm(Box), FileData((Vec, mime::Mime)), JsonWithHeaders((R, Vec<(String, String)>)), } #[derive(Debug, Eq, PartialEq)] pub enum PaymentLinkAction { PaymentLinkFormData(PaymentLinkFormData), PaymentLinkStatus(PaymentLinkStatusData), } #[derive(Debug, Eq, PartialEq, Clone, serde::Serialize, serde::Deserialize)] pub struct PaymentLinkFormData { pub js_script: String, pub css_script: String, pub sdk_url: String, } #[derive(Debug, Eq, PartialEq, Clone, serde::Serialize, serde::Deserialize)] pub struct PaymentLinkStatusData { pub js_script: String, pub css_script: String, } #[derive(Debug, Eq, PartialEq)] pub struct RedirectionFormData { pub redirect_form: RedirectForm, pub payment_method_data: Option, pub amount: String, pub currency: String, } #[derive(Debug, Eq, PartialEq)] pub enum PaymentAction { PSync, CompleteAuthorize, } #[derive(Debug, Eq, PartialEq, Serialize)] pub struct ApplicationRedirectResponse { pub url: String, } #[derive(Debug, Eq, PartialEq, Clone, serde::Serialize, serde::Deserialize)] pub enum RedirectForm { Form { endpoint: String, method: Method, form_fields: HashMap, }, Html { html_data: String, }, BlueSnap { payment_fields_token: String, // payment-field-token }, CybersourceAuthSetup { access_token: String, ddc_url: String, reference_id: String, }, CybersourceConsumerAuth { access_token: String, step_up_url: String, }, Payme, Braintree { client_token: String, card_token: String, bin: String, }, Nmi { amount: String, currency: Currency, public_key: Secret, customer_vault_id: String, order_id: String, }, } impl From<(url::Url, Method)> for RedirectForm { fn from((mut redirect_url, method): (url::Url, Method)) -> Self { let form_fields = std::collections::HashMap::from_iter( redirect_url .query_pairs() .map(|(key, value)| (key.to_string(), value.to_string())), ); // Do not include query params in the endpoint redirect_url.set_query(None); Self::Form { endpoint: redirect_url.to_string(), method, form_fields, } } } #[derive(Clone, Copy, PartialEq, Eq)] pub enum AuthFlow { Client, Merchant, } #[instrument(skip(request, payload, state, func, api_auth), fields(merchant_id))] pub async fn server_wrap_util<'a, 'b, A, U, T, Q, F, Fut, E, OErr>( flow: &'a impl router_env::types::FlowMetric, state: web::Data, request: &'a HttpRequest, payload: T, func: F, api_auth: &dyn AuthenticateAndFetch, lock_action: api_locking::LockAction, ) -> CustomResult, OErr> where F: Fn(A, U, T) -> Fut, 'b: 'a, Fut: Future, E>>, Q: Serialize + Debug + 'a + ApiEventMetric, T: Debug + Serialize + ApiEventMetric, A: AppStateInfo + Clone, E: ErrorSwitch + error_stack::Context, OErr: ResponseError + error_stack::Context + Serialize, errors::ApiErrorResponse: ErrorSwitch, { let request_id = RequestId::extract(request) .await .into_report() .attach_printable("Unable to extract request id from request") .change_context(errors::ApiErrorResponse::InternalServerError.switch())?; let mut request_state = state.get_ref().clone(); request_state.add_request_id(request_id); let start_instant = Instant::now(); let serialized_request = masking::masked_serialize(&payload) .into_report() .attach_printable("Failed to serialize json request") .change_context(errors::ApiErrorResponse::InternalServerError.switch())?; let mut event_type = payload.get_api_event_type(); // Currently auth failures are not recorded as API events let (auth_out, auth_type) = api_auth .authenticate_and_fetch(request.headers(), &request_state) .await .switch()?; let merchant_id = auth_type .get_merchant_id() .unwrap_or("MERCHANT_ID_NOT_FOUND") .to_string(); request_state.add_merchant_id(Some(merchant_id.clone())); request_state.add_flow_name(flow.to_string()); tracing::Span::current().record("merchant_id", &merchant_id); let output = { lock_action .clone() .perform_locking_action(&request_state, merchant_id.to_owned()) .await .switch()?; let res = func(request_state.clone(), auth_out, payload) .await .switch(); lock_action .free_lock_action(&request_state, merchant_id.to_owned()) .await .switch()?; res }; let request_duration = Instant::now() .saturating_duration_since(start_instant) .as_millis(); let mut serialized_response = None; let mut error = None; let mut overhead_latency = None; let status_code = match output.as_ref() { Ok(res) => { if let ApplicationResponse::Json(data) = res { serialized_response.replace( masking::masked_serialize(&data) .into_report() .attach_printable("Failed to serialize json response") .change_context(errors::ApiErrorResponse::InternalServerError.switch())?, ); } else if let ApplicationResponse::JsonWithHeaders((data, headers)) = res { serialized_response.replace( masking::masked_serialize(&data) .into_report() .attach_printable("Failed to serialize json response") .change_context(errors::ApiErrorResponse::InternalServerError.switch())?, ); if let Some((_, value)) = headers.iter().find(|(key, _)| key == X_HS_LATENCY) { if let Ok(external_latency) = value.parse::() { overhead_latency.replace(external_latency); } } } event_type = res.get_api_event_type().or(event_type); metrics::request::track_response_status_code(res) } Err(err) => { error.replace( serde_json::to_value(err.current_context()) .into_report() .attach_printable("Failed to serialize json response") .change_context(errors::ApiErrorResponse::InternalServerError.switch()) .ok() .into(), ); err.current_context().status_code().as_u16().into() } }; let api_event = ApiEvent::new( Some(merchant_id.clone()), flow, &request_id, request_duration, status_code, serialized_request, serialized_response, overhead_latency, auth_type, error, event_type.unwrap_or(ApiEventsType::Miscellaneous), request, request.method(), ); match api_event.clone().try_into() { Ok(event) => { state.event_handler().log_event(event); } Err(err) => { logger::error!(error=?err, event=?api_event, "Error Logging API Event"); } } metrics::request::status_code_metrics(status_code, flow.to_string(), merchant_id.to_string()); output } #[instrument( skip(request, state, func, api_auth, payload), fields(request_method, request_url_path) )] pub async fn server_wrap<'a, A, T, U, Q, F, Fut, E>( flow: impl router_env::types::FlowMetric, state: web::Data, request: &'a HttpRequest, payload: T, func: F, api_auth: &dyn AuthenticateAndFetch, lock_action: api_locking::LockAction, ) -> HttpResponse where F: Fn(A, U, T) -> Fut, Fut: Future, E>>, Q: Serialize + Debug + ApiEventMetric + 'a, T: Debug + Serialize + ApiEventMetric, A: AppStateInfo + Clone, ApplicationResponse: Debug, E: ErrorSwitch + error_stack::Context, { let request_method = request.method().as_str(); let url_path = request.path(); tracing::Span::current().record("request_method", request_method); tracing::Span::current().record("request_url_path", url_path); let start_instant = Instant::now(); logger::info!(tag = ?Tag::BeginRequest, payload = ?payload); let res = match metrics::request::record_request_time_metric( server_wrap_util( &flow, state.clone(), request, payload, func, api_auth, lock_action, ), &flow, ) .await .map(|response| { logger::info!(api_response =? response); response }) { Ok(ApplicationResponse::Json(response)) => match serde_json::to_string(&response) { Ok(res) => http_response_json(res), Err(_) => http_response_err( r#"{ "error": { "message": "Error serializing response from connector" } }"#, ), }, Ok(ApplicationResponse::StatusOk) => http_response_ok(), Ok(ApplicationResponse::TextPlain(text)) => http_response_plaintext(text), Ok(ApplicationResponse::FileData((file_data, content_type))) => { http_response_file_data(file_data, content_type) } Ok(ApplicationResponse::JsonForRedirection(response)) => { match serde_json::to_string(&response) { Ok(res) => http_redirect_response(res, response), Err(_) => http_response_err( r#"{ "error": { "message": "Error serializing response from connector" } }"#, ), } } Ok(ApplicationResponse::Form(redirection_data)) => { let config = state.conf(); build_redirection_form( &redirection_data.redirect_form, redirection_data.payment_method_data, redirection_data.amount, redirection_data.currency, config, ) .respond_to(request) .map_into_boxed_body() } Ok(ApplicationResponse::PaymenkLinkForm(boxed_payment_link_data)) => { match *boxed_payment_link_data { PaymentLinkAction::PaymentLinkFormData(payment_link_data) => { match build_payment_link_html(payment_link_data) { Ok(rendered_html) => http_response_html_data(rendered_html), Err(_) => http_response_err( r#"{ "error": { "message": "Error while rendering payment link html page" } }"#, ), } } PaymentLinkAction::PaymentLinkStatus(payment_link_data) => { match get_payment_link_status(payment_link_data) { Ok(rendered_html) => http_response_html_data(rendered_html), Err(_) => http_response_err( r#"{ "error": { "message": "Error while rendering payment link status page" } }"#, ), } } } } Ok(ApplicationResponse::JsonWithHeaders((response, headers))) => { let request_elapsed_time = request.headers().get(X_HS_LATENCY).and_then(|value| { if value == "true" { Some(start_instant.elapsed()) } else { None } }); match serde_json::to_string(&response) { Ok(res) => http_response_json_with_headers(res, headers, request_elapsed_time), Err(_) => http_response_err( r#"{ "error": { "message": "Error serializing response from connector" } }"#, ), } } Err(error) => log_and_return_error_response(error), }; let response_code = res.status().as_u16(); let end_instant = Instant::now(); let request_duration = end_instant.saturating_duration_since(start_instant); logger::info!( tag = ?Tag::EndRequest, status_code = response_code, time_taken_ms = request_duration.as_millis(), ); res } pub fn log_and_return_error_response(error: Report) -> HttpResponse where T: error_stack::Context + Clone + ResponseError, Report: EmbedError, { logger::error!(?error); HttpResponse::from_error(error.embed().current_context().clone()) } pub trait EmbedError: Sized { fn embed(self) -> Self { self } } impl EmbedError for Report { fn embed(self) -> Self { #[cfg(feature = "detailed_errors")] { let mut report = self; let error_trace = serde_json::to_value(&report).ok().and_then(|inner| { serde_json::from_value::>>(inner) .ok() .map(Into::>::into) .map(serde_json::to_value) .transpose() .ok() .flatten() }); match report.downcast_mut::() { None => {} Some(inner) => { inner.get_internal_error_mut().stacktrace = error_trace; } } report } #[cfg(not(feature = "detailed_errors"))] self } } pub fn http_response_json(response: T) -> HttpResponse { HttpResponse::Ok() .content_type(mime::APPLICATION_JSON) .body(response) } pub fn http_server_error_json_response( response: T, ) -> HttpResponse { HttpResponse::InternalServerError() .content_type(mime::APPLICATION_JSON) .body(response) } pub fn http_response_json_with_headers( response: T, mut headers: Vec<(String, String)>, request_duration: Option, ) -> HttpResponse { let mut response_builder = HttpResponse::Ok(); for (name, value) in headers.iter_mut() { if name == X_HS_LATENCY { if let Some(request_duration) = request_duration { if let Ok(external_latency) = value.parse::() { let updated_duration = request_duration.as_millis() - external_latency; *value = updated_duration.to_string(); } } } response_builder.append_header((name.clone(), value.clone())); } response_builder .content_type(mime::APPLICATION_JSON) .body(response) } pub fn http_response_plaintext(res: T) -> HttpResponse { HttpResponse::Ok().content_type(mime::TEXT_PLAIN).body(res) } pub fn http_response_file_data( res: T, content_type: mime::Mime, ) -> HttpResponse { HttpResponse::Ok().content_type(content_type).body(res) } pub fn http_response_html_data(res: T) -> HttpResponse { HttpResponse::Ok().content_type(mime::TEXT_HTML).body(res) } pub fn http_response_ok() -> HttpResponse { HttpResponse::Ok().finish() } pub fn http_redirect_response( response: T, redirection_response: api::RedirectionResponse, ) -> HttpResponse { HttpResponse::Ok() .content_type(mime::APPLICATION_JSON) .append_header(( "Location", redirection_response.return_url_with_query_params, )) .status(http::StatusCode::FOUND) .body(response) } pub fn http_response_err(response: T) -> HttpResponse { HttpResponse::BadRequest() .content_type(mime::APPLICATION_JSON) .body(response) } pub trait ConnectorRedirectResponse { fn get_flow_type( &self, _query_params: &str, _json_payload: Option, _action: PaymentAction, ) -> CustomResult { Ok(payments::CallConnectorAction::Avoid) } } pub trait Authenticate { fn get_client_secret(&self) -> Option<&String> { None } } impl Authenticate for api_models::payments::PaymentsRequest { fn get_client_secret(&self) -> Option<&String> { self.client_secret.as_ref() } } impl Authenticate for api_models::payment_methods::PaymentMethodListRequest { fn get_client_secret(&self) -> Option<&String> { self.client_secret.as_ref() } } impl Authenticate for api_models::payments::PaymentsSessionRequest { fn get_client_secret(&self) -> Option<&String> { Some(&self.client_secret) } } impl Authenticate for api_models::payments::PaymentsRetrieveRequest {} impl Authenticate for api_models::payments::PaymentsCancelRequest {} impl Authenticate for api_models::payments::PaymentsCaptureRequest {} impl Authenticate for api_models::payments::PaymentsIncrementalAuthorizationRequest {} impl Authenticate for api_models::payments::PaymentsStartRequest {} // impl Authenticate for api_models::payments::PaymentsApproveRequest {} impl Authenticate for api_models::payments::PaymentsRejectRequest {} pub fn build_redirection_form( form: &RedirectForm, payment_method_data: Option, amount: String, currency: String, config: Settings, ) -> maud::Markup { use maud::PreEscaped; match form { RedirectForm::Form { endpoint, method, form_fields, } => maud::html! { (maud::DOCTYPE) html { meta name="viewport" content="width=device-width, initial-scale=1"; head { style { r##" "## } (PreEscaped(r##" "##)) } body style="background-color: #ffffff; padding: 20px; font-family: Arial, Helvetica, Sans-Serif;" { div id="loader1" class="lottie" style="height: 150px; display: block; position: relative; margin-left: auto; margin-right: auto;" { "" } (PreEscaped(r#""#)) (PreEscaped(r#" "#)) h3 style="text-align: center;" { "Please wait while we process your payment..." } form action=(PreEscaped(endpoint)) method=(method.to_string()) #payment_form { @for (field, value) in form_fields { input type="hidden" name=(field) value=(value); } } (PreEscaped(r#""#)) } } }, RedirectForm::Html { html_data } => PreEscaped(html_data.to_string()), RedirectForm::BlueSnap { payment_fields_token, } => { let card_details = if let Some(api::PaymentMethodData::Card(ccard)) = payment_method_data { format!( "var saveCardDirectly={{cvv: \"{}\",amount: {},currency: \"{}\"}};", ccard.card_cvc.peek(), amount, currency ) } else { "".to_string() }; let bluesnap_sdk_url = config.connectors.bluesnap.secondary_base_url; maud::html! { (maud::DOCTYPE) html { head { meta name="viewport" content="width=device-width, initial-scale=1"; (PreEscaped(format!(""))) } body style="background-color: #ffffff; padding: 20px; font-family: Arial, Helvetica, Sans-Serif;" { div id="loader1" class="lottie" style="height: 150px; display: block; position: relative; margin-top: 150px; margin-left: auto; margin-right: auto;" { "" } (PreEscaped(r#""#)) (PreEscaped(r#" "#)) h3 style="text-align: center;" { "Please wait while we process your payment..." } } (PreEscaped(format!(" "))) }} } RedirectForm::CybersourceAuthSetup { access_token, ddc_url, reference_id, } => { maud::html! { (maud::DOCTYPE) html { head { meta name="viewport" content="width=device-width, initial-scale=1"; } body style="background-color: #ffffff; padding: 20px; font-family: Arial, Helvetica, Sans-Serif;" { div id="loader1" class="lottie" style="height: 150px; display: block; position: relative; margin-top: 150px; margin-left: auto; margin-right: auto;" { "" } (PreEscaped(r#""#)) (PreEscaped(r#" "#)) h3 style="text-align: center;" { "Please wait while we process your payment..." } } (PreEscaped(r#""#)) (PreEscaped(format!("
"))) (PreEscaped(r#""#)) (PreEscaped(format!(" "))) }} } RedirectForm::CybersourceConsumerAuth { access_token, step_up_url, } => { maud::html! { (maud::DOCTYPE) html { head { meta name="viewport" content="width=device-width, initial-scale=1"; } body style="background-color: #ffffff; padding: 20px; font-family: Arial, Helvetica, Sans-Serif;" { div id="loader1" class="lottie" style="height: 150px; display: block; position: relative; margin-top: 150px; margin-left: auto; margin-right: auto;" { "" } (PreEscaped(r#""#)) (PreEscaped(r#" "#)) h3 style="text-align: center;" { "Please wait while we process your payment..." } } // This is the iframe recommended by cybersource but the redirection happens inside this iframe once otp // is received and we lose control of the redirection on user client browser, so to avoid that we have removed this iframe and directly consumed it. // (PreEscaped(r#""#)) (PreEscaped(format!("
"))) (PreEscaped(r#""#)) }} } RedirectForm::Payme => { maud::html! { (maud::DOCTYPE) head { (PreEscaped(r#""#)) } (PreEscaped(" ".to_string())) } } RedirectForm::Braintree { client_token, card_token, bin, } => { maud::html! { (maud::DOCTYPE) html { head { meta name="viewport" content="width=device-width, initial-scale=1"; (PreEscaped(r#""#)) // (PreEscaped(r#""#)) } body style="background-color: #ffffff; padding: 20px; font-family: Arial, Helvetica, Sans-Serif;" { div id="loader1" class="lottie" style="height: 150px; display: block; position: relative; margin-top: 150px; margin-left: auto; margin-right: auto;" { "" } (PreEscaped(r#""#)) (PreEscaped(r#" "#)) h3 style="text-align: center;" { "Please wait while we process your payment..." } } (PreEscaped(format!("" ))) }} } RedirectForm::Nmi { amount, currency, public_key, customer_vault_id, order_id, } => { let public_key_val = public_key.peek(); maud::html! { (maud::DOCTYPE) head { (PreEscaped(r#""#)) } (PreEscaped(format!("" ))) } } } } pub fn build_payment_link_html( payment_link_data: PaymentLinkFormData, ) -> CustomResult { let mut tera = Tera::default(); // Add modification to css template with dynamic data let css_template = include_str!("../core/payment_link/payment_link_initiate/payment_link.css").to_string(); let _ = tera.add_raw_template("payment_link_css", &css_template); let mut context = Context::new(); context.insert("css_color_scheme", &payment_link_data.css_script); let rendered_css = match tera.render("payment_link_css", &context) { Ok(rendered_css) => rendered_css, Err(tera_error) => { crate::logger::warn!("{tera_error}"); Err(errors::ApiErrorResponse::InternalServerError)? } }; // Add modification to js template with dynamic data let js_template = include_str!("../core/payment_link/payment_link_initiate/payment_link.js").to_string(); let _ = tera.add_raw_template("payment_link_js", &js_template); context.insert("payment_details_js_script", &payment_link_data.js_script); let rendered_js = match tera.render("payment_link_js", &context) { Ok(rendered_js) => rendered_js, Err(tera_error) => { crate::logger::warn!("{tera_error}"); Err(errors::ApiErrorResponse::InternalServerError)? } }; // Modify Html template with rendered js and rendered css files let html_template = include_str!("../core/payment_link/payment_link_initiate/payment_link.html").to_string(); let _ = tera.add_raw_template("payment_link", &html_template); context.insert( "hyperloader_sdk_link", &get_hyper_loader_sdk(&payment_link_data.sdk_url), ); context.insert("rendered_css", &rendered_css); context.insert("rendered_js", &rendered_js); match tera.render("payment_link", &context) { Ok(rendered_html) => Ok(rendered_html), Err(tera_error) => { crate::logger::warn!("{tera_error}"); Err(errors::ApiErrorResponse::InternalServerError)? } } } fn get_hyper_loader_sdk(sdk_url: &str) -> String { format!("") } pub fn get_payment_link_status( payment_link_data: PaymentLinkStatusData, ) -> CustomResult { let mut tera = Tera::default(); // Add modification to css template with dynamic data let css_template = include_str!("../core/payment_link/payment_link_status/status.css").to_string(); let _ = tera.add_raw_template("payment_link_css", &css_template); let mut context = Context::new(); context.insert("css_color_scheme", &payment_link_data.css_script); let rendered_css = match tera.render("payment_link_css", &context) { Ok(rendered_css) => rendered_css, Err(tera_error) => { crate::logger::warn!("{tera_error}"); Err(errors::ApiErrorResponse::InternalServerError)? } }; // Add modification to js template with dynamic data let js_template = include_str!("../core/payment_link/payment_link_status/status.js").to_string(); let _ = tera.add_raw_template("payment_link_js", &js_template); context.insert("payment_details_js_script", &payment_link_data.js_script); let rendered_js = match tera.render("payment_link_js", &context) { Ok(rendered_js) => rendered_js, Err(tera_error) => { crate::logger::warn!("{tera_error}"); Err(errors::ApiErrorResponse::InternalServerError)? } }; // Modify Html template with rendered js and rendered css files let html_template = include_str!("../core/payment_link/payment_link_status/status.html").to_string(); let _ = tera.add_raw_template("payment_link_status", &html_template); context.insert("rendered_css", &rendered_css); context.insert("rendered_js", &rendered_js); match tera.render("payment_link_status", &context) { Ok(rendered_html) => Ok(rendered_html), Err(tera_error) => { crate::logger::warn!("{tera_error}"); Err(errors::ApiErrorResponse::InternalServerError)? } } } #[cfg(test)] mod tests { #[test] fn test_mime_essence() { assert_eq!(mime::APPLICATION_JSON.essence_str(), "application/json"); } }