Skip to content

Commit

Permalink
feat(error): add feature-gated stacktrace to error received from API (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
NishantJoshi00 authored May 13, 2023
1 parent daf1f2e commit bf2352b
Show file tree
Hide file tree
Showing 9 changed files with 117 additions and 2 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/api_models/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ errors = [
]
multiple_mca = []
dummy_connector = []
detailed_errors = []

[dependencies]
actix-web = { version = "4.3.1", optional = true }
Expand Down
27 changes: 27 additions & 0 deletions crates/api_models/src/errors/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ pub struct ApiError {
pub error_identifier: u16,
pub error_message: String,
pub extra: Option<Extra>,
#[cfg(feature = "detailed_errors")]
pub stacktrace: Option<serde_json::Value>,
}

impl ApiError {
Expand All @@ -29,6 +31,8 @@ impl ApiError {
error_identifier,
error_message: error_message.to_string(),
extra,
#[cfg(feature = "detailed_errors")]
stacktrace: None,
}
}
}
Expand All @@ -41,6 +45,9 @@ struct ErrorResponse<'a> {
code: String,
#[serde(flatten)]
extra: &'a Option<Extra>,
#[cfg(feature = "detailed_errors")]
#[serde(skip_serializing_if = "Option::is_none")]
stacktrace: Option<&'a serde_json::Value>,
}

impl<'a> From<&'a ApiErrorResponse> for ErrorResponse<'a> {
Expand All @@ -52,6 +59,9 @@ impl<'a> From<&'a ApiErrorResponse> for ErrorResponse<'a> {
message: Cow::Borrowed(value.get_internal_error().error_message.as_str()),
error_type,
extra: &error_info.extra,

#[cfg(feature = "detailed_errors")]
stacktrace: error_info.stacktrace.as_ref(),
}
}
}
Expand Down Expand Up @@ -114,6 +124,23 @@ impl ApiErrorResponse {
}
}

pub fn get_internal_error_mut(&mut self) -> &mut ApiError {
match self {
Self::Unauthorized(i)
| Self::ForbiddenCommonResource(i)
| Self::ForbiddenPrivateResource(i)
| Self::Conflict(i)
| Self::Gone(i)
| Self::Unprocessable(i)
| Self::InternalServerError(i)
| Self::NotImplemented(i)
| Self::NotFound(i)
| Self::MethodNotAllowed(i)
| Self::BadRequest(i)
| Self::ConnectorError(i, _) => i,
}
}

pub(crate) fn error_type(&self) -> &'static str {
match self {
Self::Unauthorized(_)
Expand Down
1 change: 1 addition & 0 deletions crates/router/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ vergen = ["router_env/vergen"]
multiple_mca = ["api_models/multiple_mca"]
dummy_connector = ["api_models/dummy_connector"]
external_access_dc = ["dummy_connector"]
detailed_errors = ["api_models/detailed_errors", "error-stack/serde"]


[dependencies]
Expand Down
2 changes: 2 additions & 0 deletions crates/router/src/compatibility/stripe/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -609,3 +609,5 @@ impl common_utils::errors::ErrorSwitch<StripeErrorCode> for errors::ApiErrorResp
self.clone().into()
}
}

impl crate::services::EmbedError for error_stack::Report<StripeErrorCode> {}
3 changes: 2 additions & 1 deletion crates/router/src/compatibility/wrap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use serde::Serialize;
use crate::{
core::errors::{self, RouterResult},
routes::app::AppStateInfo,
services::{api, authentication as auth, logger},
services::{self, api, authentication as auth, logger},
};

#[instrument(skip(request, payload, state, func, api_authentication))]
Expand All @@ -25,6 +25,7 @@ where
Q: Serialize + std::fmt::Debug + 'a,
S: TryFrom<Q> + Serialize,
E: Serialize + error_stack::Context + actix_web::ResponseError + Clone,
error_stack::Report<E>: services::EmbedError,
errors::ApiErrorResponse: ErrorSwitch<E>,
T: std::fmt::Debug,
A: AppStateInfo,
Expand Down
44 changes: 44 additions & 0 deletions crates/router/src/core/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -461,3 +461,47 @@ pub enum WebhooksFlowError {
#[error("Dispute webhook status validation failed")]
DisputeWebhookValidationFailed,
}

#[cfg(feature = "detailed_errors")]
pub mod error_stack_parsing {

#[derive(serde::Deserialize)]
pub struct NestedErrorStack<'a> {
context: std::borrow::Cow<'a, str>,
attachments: Vec<std::borrow::Cow<'a, str>>,
sources: Vec<NestedErrorStack<'a>>,
}

#[derive(serde::Serialize, Debug)]
struct LinearErrorStack<'a> {
context: std::borrow::Cow<'a, str>,
#[serde(skip_serializing_if = "Vec::is_empty")]
attachments: Vec<std::borrow::Cow<'a, str>>,
}

#[derive(serde::Serialize, Debug)]
pub struct VecLinearErrorStack<'a>(Vec<LinearErrorStack<'a>>);

impl<'a> From<Vec<NestedErrorStack<'a>>> for VecLinearErrorStack<'a> {
fn from(value: Vec<NestedErrorStack<'a>>) -> Self {
let multi_layered_errors: Vec<_> = value
.into_iter()
.flat_map(|current_error| {
[LinearErrorStack {
context: current_error.context,
attachments: current_error.attachments,
}]
.into_iter()
.chain(
Into::<VecLinearErrorStack<'a>>::into(current_error.sources)
.0
.into_iter(),
)
})
.collect();
Self(multi_layered_errors)
}
}
}
#[cfg(feature = "detailed_errors")]
pub use error_stack_parsing::*;
2 changes: 2 additions & 0 deletions crates/router/src/core/errors/api_error_response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,8 @@ impl actix_web::ResponseError for ApiErrorResponse {
}
}

impl crate::services::EmbedError for error_stack::Report<ApiErrorResponse> {}

impl common_utils::errors::ErrorSwitch<api_models::errors::types::ApiErrorResponse>
for ApiErrorResponse
{
Expand Down
38 changes: 37 additions & 1 deletion crates/router/src/services/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -630,9 +630,45 @@ where
pub fn log_and_return_error_response<T>(error: Report<T>) -> HttpResponse
where
T: actix_web::ResponseError + error_stack::Context + Clone,
Report<T>: EmbedError,
{
logger::error!(?error);
HttpResponse::from_error(error.current_context().clone())
HttpResponse::from_error(error.embed().current_context().clone())
}

pub trait EmbedError: Sized {
fn embed(self) -> Self {
self
}
}

impl EmbedError for Report<api_models::errors::types::ApiErrorResponse> {
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::<Vec<errors::NestedErrorStack<'_>>>(inner)
.ok()
.map(Into::<errors::VecLinearErrorStack<'_>>::into)
.map(serde_json::to_value)
.transpose()
.ok()
.flatten()
});

match report.downcast_mut::<api_models::errors::types::ApiErrorResponse>() {
None => {}
Some(inner) => {
inner.get_internal_error_mut().stacktrace = error_trace;
}
}
report
}

#[cfg(not(feature = "detailed_errors"))]
self
}
}

pub fn http_response_json<T: body::MessageBody + 'static>(response: T) -> HttpResponse {
Expand Down

0 comments on commit bf2352b

Please sign in to comment.