Skip to content

Commit 9a5255f

Browse files
committed
Rework the error fallback to better report the error to Sentry
This means we keep the std::error::Error boxed longer, and transform it into an error context later
1 parent 5cbb5d9 commit 9a5255f

File tree

25 files changed

+214
-142
lines changed

25 files changed

+214
-142
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/axum-utils/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ publish = false
1212
workspace = true
1313

1414
[dependencies]
15+
anyhow.workspace = true
1516
axum.workspace = true
1617
axum-extra.workspace = true
1718
base64ct.workspace = true

crates/axum-utils/src/error_wrapper.rs

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,8 @@
55
// Please see LICENSE in the repository root for full details.
66

77
use axum::response::{IntoResponse, Response};
8-
use http::StatusCode;
98

10-
use crate::record_error;
9+
use crate::InternalError;
1110

1211
/// A simple wrapper around an error that implements [`IntoResponse`].
1312
#[derive(Debug, thiserror::Error)]
@@ -19,13 +18,6 @@ where
1918
T: std::error::Error + 'static,
2019
{
2120
fn into_response(self) -> Response {
22-
// TODO: make this a bit more user friendly
23-
let sentry_event_id = record_error!(self.0);
24-
(
25-
StatusCode::INTERNAL_SERVER_ERROR,
26-
sentry_event_id,
27-
self.0.to_string(),
28-
)
29-
.into_response()
21+
InternalError::from(self.0).into_response()
3022
}
3123
}

crates/axum-utils/src/fancy_error.rs

Lines changed: 67 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -15,55 +15,91 @@ use mas_templates::ErrorContext;
1515

1616
use crate::sentry::SentryEventID;
1717

18-
pub struct FancyError {
19-
context: ErrorContext,
18+
fn build_context(mut err: &dyn std::error::Error) -> ErrorContext {
19+
let description = err.to_string();
20+
let mut details = Vec::new();
21+
while let Some(source) = err.source() {
22+
err = source;
23+
details.push(err.to_string());
24+
}
25+
26+
ErrorContext::new()
27+
.with_description(description)
28+
.with_details(details.join("\n"))
2029
}
2130

22-
impl FancyError {
23-
#[must_use]
24-
pub fn new(context: ErrorContext) -> Self {
25-
Self { context }
31+
pub struct GenericError {
32+
error: Box<dyn std::error::Error + 'static>,
33+
code: StatusCode,
34+
}
35+
36+
impl IntoResponse for GenericError {
37+
fn into_response(self) -> Response {
38+
tracing::warn!(message = &*self.error);
39+
let context = build_context(&*self.error);
40+
let context_text = format!("{context}");
41+
42+
(
43+
self.code,
44+
TypedHeader(ContentType::text()),
45+
Extension(context),
46+
context_text,
47+
)
48+
.into_response()
2649
}
2750
}
2851

29-
impl std::fmt::Display for FancyError {
30-
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31-
let code = self.context.code().unwrap_or("Internal error");
32-
match (self.context.description(), self.context.details()) {
33-
(Some(description), Some(details)) => {
34-
write!(f, "{code}: {description} ({details})")
35-
}
36-
(Some(message), None) | (None, Some(message)) => {
37-
write!(f, "{code}: {message}")
38-
}
39-
(None, None) => {
40-
write!(f, "{code}")
41-
}
52+
impl GenericError {
53+
pub fn new(code: StatusCode, err: impl std::error::Error + 'static) -> Self {
54+
Self {
55+
error: Box::new(err),
56+
code,
4257
}
4358
}
4459
}
4560

46-
impl<E: std::fmt::Debug + std::fmt::Display> From<E> for FancyError {
47-
fn from(err: E) -> Self {
48-
let context = ErrorContext::new()
49-
.with_description(format!("{err}"))
50-
.with_details(format!("{err:?}"));
51-
FancyError { context }
52-
}
61+
pub struct InternalError {
62+
error: Box<dyn std::error::Error + 'static>,
5363
}
5464

55-
impl IntoResponse for FancyError {
65+
impl IntoResponse for InternalError {
5666
fn into_response(self) -> Response {
57-
tracing::error!(message = %self.context);
58-
let error = format!("{}", self.context);
67+
tracing::error!(message = &*self.error);
5968
let event_id = SentryEventID::for_last_event();
69+
let context = build_context(&*self.error);
70+
let context_text = format!("{context}");
71+
6072
(
6173
StatusCode::INTERNAL_SERVER_ERROR,
6274
TypedHeader(ContentType::text()),
6375
event_id,
64-
Extension(self.context),
65-
error,
76+
Extension(context),
77+
context_text,
6678
)
6779
.into_response()
6880
}
6981
}
82+
83+
impl<E: std::error::Error + 'static> From<E> for InternalError {
84+
fn from(err: E) -> Self {
85+
Self {
86+
error: Box::new(err),
87+
}
88+
}
89+
}
90+
91+
impl InternalError {
92+
/// Create a new error from a boxed error
93+
#[must_use]
94+
pub fn new(error: Box<dyn std::error::Error + 'static>) -> Self {
95+
Self { error }
96+
}
97+
98+
/// Create a new error from an [`anyhow::Error`]
99+
#[must_use]
100+
pub fn from_anyhow(err: anyhow::Error) -> Self {
101+
Self {
102+
error: err.into_boxed_dyn_error(),
103+
}
104+
}
105+
}

crates/axum-utils/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,6 @@ pub use axum;
2222

2323
pub use self::{
2424
error_wrapper::ErrorWrapper,
25-
fancy_error::FancyError,
25+
fancy_error::{GenericError, InternalError},
2626
session::{SessionInfo, SessionInfoExt},
2727
};

crates/handlers/src/admin/mod.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ use axum::{
1919
};
2020
use hyper::header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE};
2121
use indexmap::IndexMap;
22-
use mas_axum_utils::FancyError;
22+
use mas_axum_utils::InternalError;
2323
use mas_http::CorsLayerExt;
2424
use mas_matrix::HomeserverConnection;
2525
use mas_policy::PolicyFactory;
@@ -180,7 +180,7 @@ where
180180
async fn swagger(
181181
State(url_builder): State<UrlBuilder>,
182182
State(templates): State<Templates>,
183-
) -> Result<Html<String>, FancyError> {
183+
) -> Result<Html<String>, InternalError> {
184184
let ctx = ApiDocContext::from_url_builder(&url_builder);
185185
let res = templates.render_swagger(&ctx)?;
186186
Ok(Html(res))
@@ -189,7 +189,7 @@ async fn swagger(
189189
async fn swagger_callback(
190190
State(url_builder): State<UrlBuilder>,
191191
State(templates): State<Templates>,
192-
) -> Result<Html<String>, FancyError> {
192+
) -> Result<Html<String>, InternalError> {
193193
let ctx = ApiDocContext::from_url_builder(&url_builder);
194194
let res = templates.render_swagger_callback(&ctx)?;
195195
Ok(Html(res))

crates/handlers/src/compat/login_sso_complete.rs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use axum::{
1313
};
1414
use chrono::Duration;
1515
use mas_axum_utils::{
16-
FancyError,
16+
InternalError,
1717
cookies::CookieJar,
1818
csrf::{CsrfExt, ProtectedForm},
1919
};
@@ -59,7 +59,7 @@ pub async fn get(
5959
cookie_jar: CookieJar,
6060
Path(id): Path<Ulid>,
6161
Query(params): Query<Params>,
62-
) -> Result<Response, FancyError> {
62+
) -> Result<Response, InternalError> {
6363
let (cookie_jar, maybe_session) = match load_session_or_fallback(
6464
cookie_jar, &clock, &mut rng, &templates, &locale, &mut repo,
6565
)
@@ -93,7 +93,8 @@ pub async fn get(
9393
.compat_sso_login()
9494
.lookup(id)
9595
.await?
96-
.context("Could not find compat SSO login")?;
96+
.context("Could not find compat SSO login")
97+
.map_err(InternalError::from_anyhow)?;
9798

9899
// Bail out if that login session is more than 30min old
99100
if clock.now() > login.created_at + Duration::microseconds(30 * 60 * 1000 * 1000) {
@@ -132,7 +133,7 @@ pub async fn post(
132133
Path(id): Path<Ulid>,
133134
Query(params): Query<Params>,
134135
Form(form): Form<ProtectedForm<()>>,
135-
) -> Result<Response, FancyError> {
136+
) -> Result<Response, InternalError> {
136137
let (cookie_jar, maybe_session) = match load_session_or_fallback(
137138
cookie_jar, &clock, &mut rng, &templates, &locale, &mut repo,
138139
)
@@ -166,7 +167,8 @@ pub async fn post(
166167
.compat_sso_login()
167168
.lookup(id)
168169
.await?
169-
.context("Could not find compat SSO login")?;
170+
.context("Could not find compat SSO login")
171+
.map_err(InternalError::from_anyhow)?;
170172

171173
// Bail out if that login session isn't pending, or is more than 30min old
172174
if !login.is_pending()

crates/handlers/src/graphql/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ use futures_util::TryStreamExt;
2626
use headers::{Authorization, ContentType, HeaderValue, authorization::Bearer};
2727
use hyper::header::CACHE_CONTROL;
2828
use mas_axum_utils::{
29-
FancyError, SessionInfo, SessionInfoExt, cookies::CookieJar, sentry::SentryEventID,
29+
InternalError, SessionInfo, SessionInfoExt, cookies::CookieJar, sentry::SentryEventID,
3030
};
3131
use mas_data_model::{BrowserSession, Session, SiteConfig, User};
3232
use mas_matrix::HomeserverConnection;
@@ -383,7 +383,7 @@ pub async fn get(
383383
authorization: Option<TypedHeader<Authorization<Bearer>>>,
384384
user_agent: Option<TypedHeader<headers::UserAgent>>,
385385
RawQuery(query): RawQuery,
386-
) -> Result<impl IntoResponse, FancyError> {
386+
) -> Result<impl IntoResponse, InternalError> {
387387
let token = authorization
388388
.as_ref()
389389
.map(|TypedHeader(Authorization(bearer))| bearer.token());

crates/handlers/src/health.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@
55
// Please see LICENSE in the repository root for full details.
66

77
use axum::{extract::State, response::IntoResponse};
8-
use mas_axum_utils::FancyError;
8+
use mas_axum_utils::InternalError;
99
use sqlx::PgPool;
1010
use tracing::{Instrument, info_span};
1111

12-
pub async fn get(State(pool): State<PgPool>) -> Result<impl IntoResponse, FancyError> {
12+
pub async fn get(State(pool): State<PgPool>) -> Result<impl IntoResponse, InternalError> {
1313
let mut conn = pool.acquire().await?;
1414

1515
sqlx::query("SELECT $1")

crates/handlers/src/lib.rs

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ use hyper::{
3535
ACCEPT, ACCEPT_LANGUAGE, AUTHORIZATION, CONTENT_LANGUAGE, CONTENT_LENGTH, CONTENT_TYPE,
3636
},
3737
};
38-
use mas_axum_utils::{FancyError, cookies::CookieJar};
38+
use mas_axum_utils::{InternalError, cookies::CookieJar};
3939
use mas_data_model::SiteConfig;
4040
use mas_http::CorsLayerExt;
4141
use mas_keystore::{Encrypter, Keystore};
@@ -437,16 +437,14 @@ where
437437
)
438438
.layer(AndThenLayer::new(
439439
async move |response: axum::response::Response| {
440-
if response.status().is_server_error() {
441-
// Error responses should have an ErrorContext attached to them
442-
let ext = response.extensions().get::<ErrorContext>();
443-
if let Some(ctx) = ext {
444-
if let Ok(res) = templates.render_error(ctx) {
445-
let (mut parts, _original_body) = response.into_parts();
446-
parts.headers.remove(CONTENT_TYPE);
447-
parts.headers.remove(CONTENT_LENGTH);
448-
return Ok((parts, Html(res)).into_response());
449-
}
440+
// Error responses should have an ErrorContext attached to them
441+
let ext = response.extensions().get::<ErrorContext>();
442+
if let Some(ctx) = ext {
443+
if let Ok(res) = templates.render_error(ctx) {
444+
let (mut parts, _original_body) = response.into_parts();
445+
parts.headers.remove(CONTENT_TYPE);
446+
parts.headers.remove(CONTENT_LENGTH);
447+
return Ok((parts, Html(res)).into_response());
450448
}
451449
}
452450

@@ -466,7 +464,7 @@ pub async fn fallback(
466464
method: Method,
467465
version: Version,
468466
PreferredLanguage(locale): PreferredLanguage,
469-
) -> Result<impl IntoResponse, FancyError> {
467+
) -> Result<impl IntoResponse, InternalError> {
470468
let ctx = NotFoundContext::new(&method, version, &uri).with_language(locale);
471469
// XXX: this should look at the Accept header and return JSON if requested
472470

0 commit comments

Comments
 (0)