}
Err(error) => {
assert!(
- error.error_type.eq(&Some(expected_err.clone())),
+ error.error_type.eq(&expected_err.clone()),
"Got Err {:?}, but should have failed with message: {} for reason: {}. invalid_payloads.nth({})",
error.error_type,
expected_err,
}
Err(error) => {
assert!(
- error.error_type.eq(&Some(expected_err.clone())),
+ error.error_type.eq(&expected_err.clone()),
"Got Err {:?}, but should have failed with message: {} for reason: {}. invalid_payloads.nth({})",
error.error_type,
expected_err,
pub type LemmyResult<T> = Result<T, LemmyError>;
pub struct LemmyError {
- pub error_type: Option<LemmyErrorType>,
+ pub error_type: LemmyErrorType,
pub inner: anyhow::Error,
pub context: SpanTrace,
}
T: Into<anyhow::Error>,
{
fn from(t: T) -> Self {
+ let cause = t.into();
LemmyError {
- error_type: None,
- inner: t.into(),
+ error_type: LemmyErrorType::Unknown(format!("{}", &cause)),
+ inner: cause,
context: SpanTrace::capture(),
}
}
impl Display for LemmyError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- if let Some(message) = &self.error_type {
- write!(f, "{message}: ")?;
- }
+ write!(f, "{}: ", &self.error_type)?;
// print anyhow including trace
// https://docs.rs/anyhow/latest/anyhow/struct.Error.html#display-representations
// this will print the anyhow trace (only if it exists)
}
fn error_response(&self) -> actix_web::HttpResponse {
- if let Some(message) = &self.error_type {
- actix_web::HttpResponse::build(self.status_code()).json(message)
- } else {
- actix_web::HttpResponse::build(self.status_code())
- .content_type("text/plain")
- .body(self.inner.to_string())
- }
+ actix_web::HttpResponse::build(self.status_code()).json(&self.error_type)
}
}
CouldntCreateAudioCaptcha,
InvalidUrlScheme,
CouldntSendWebmention,
- Unknown,
+ Unknown(String),
}
impl From<LemmyErrorType> for LemmyError {
fn from(error_type: LemmyErrorType) -> Self {
let inner = anyhow::anyhow!("{}", error_type);
LemmyError {
- error_type: Some(error_type),
+ error_type,
inner,
context: SpanTrace::capture(),
}
impl<T, E: Into<anyhow::Error>> LemmyErrorExt<T, E> for Result<T, E> {
fn with_lemmy_type(self, error_type: LemmyErrorType) -> Result<T, LemmyError> {
self.map_err(|error| LemmyError {
- error_type: Some(error_type),
+ error_type,
inner: error.into(),
context: SpanTrace::capture(),
})
impl<T> LemmyErrorExt2<T> for Result<T, LemmyError> {
fn with_lemmy_type(self, error_type: LemmyErrorType) -> Result<T, LemmyError> {
self.map_err(|mut e| {
- e.error_type = Some(error_type);
+ e.error_type = error_type;
e
})
}
pub mod claims;
pub mod error;
pub mod request;
+pub mod response;
pub mod utils;
pub mod version;
--- /dev/null
+use crate::error::{LemmyError, LemmyErrorType};
+use actix_web::{
+ dev::ServiceResponse,
+ http::header,
+ middleware::ErrorHandlerResponse,
+ HttpResponse,
+};
+
+pub fn jsonify_plain_text_errors<BODY>(
+ res: ServiceResponse<BODY>,
+) -> actix_web::Result<ErrorHandlerResponse<BODY>> {
+ let maybe_error = res.response().error();
+
+ // This function is only expected to be called for errors, so if there is no error, return
+ if maybe_error.is_none() {
+ return Ok(ErrorHandlerResponse::Response(res.map_into_left_body()));
+ }
+ // We're assuming that any LemmyError is already in JSON format, so we don't need to do anything
+ if maybe_error
+ .expect("http responses with 400-599 statuses should have an error object")
+ .as_error::<LemmyError>()
+ .is_some()
+ {
+ return Ok(ErrorHandlerResponse::Response(res.map_into_left_body()));
+ }
+
+ let (req, res) = res.into_parts();
+ let error = res
+ .error()
+ .expect("expected an error object in the response");
+ let response = HttpResponse::build(res.status())
+ .append_header(header::ContentType::json())
+ .json(LemmyErrorType::Unknown(error.to_string()));
+
+ let service_response = ServiceResponse::new(req, response);
+ Ok(ErrorHandlerResponse::Response(
+ service_response.map_into_right_body(),
+ ))
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::error::{LemmyError, LemmyErrorType};
+ use actix_web::{
+ error::ErrorInternalServerError,
+ middleware::ErrorHandlers,
+ test,
+ web,
+ App,
+ Error,
+ Handler,
+ Responder,
+ };
+ use http::StatusCode;
+
+ #[actix_web::test]
+ async fn test_non_error_responses_are_not_modified() {
+ async fn ok_service() -> actix_web::Result<String, Error> {
+ Ok("Oll Korrect".to_string())
+ }
+
+ check_for_jsonification(ok_service, StatusCode::OK, "Oll Korrect").await;
+ }
+
+ #[actix_web::test]
+ async fn test_lemmy_errors_are_not_modified() {
+ async fn lemmy_error_service() -> actix_web::Result<String, LemmyError> {
+ Err(LemmyError::from(LemmyErrorType::EmailAlreadyExists))
+ }
+
+ check_for_jsonification(
+ lemmy_error_service,
+ StatusCode::BAD_REQUEST,
+ "{\"error\":\"email_already_exists\"}",
+ )
+ .await;
+ }
+
+ #[actix_web::test]
+ async fn test_generic_errors_are_jsonified_as_unknown_errors() {
+ async fn generic_error_service() -> actix_web::Result<String, Error> {
+ Err(ErrorInternalServerError("This is not a LemmyError"))
+ }
+
+ check_for_jsonification(
+ generic_error_service,
+ StatusCode::INTERNAL_SERVER_ERROR,
+ "{\"error\":\"unknown\",\"message\":\"This is not a LemmyError\"}",
+ )
+ .await;
+ }
+
+ #[actix_web::test]
+ async fn test_anyhow_errors_wrapped_in_lemmy_errors_are_jsonified_correctly() {
+ async fn anyhow_error_service() -> actix_web::Result<String, LemmyError> {
+ Err(LemmyError::from(anyhow::anyhow!("This is the inner error")))
+ }
+
+ check_for_jsonification(
+ anyhow_error_service,
+ StatusCode::BAD_REQUEST,
+ "{\"error\":\"unknown\",\"message\":\"This is the inner error\"}",
+ )
+ .await;
+ }
+
+ async fn check_for_jsonification(
+ service: impl Handler<(), Output = impl Responder + 'static>,
+ expected_status_code: StatusCode,
+ expected_body: &str,
+ ) {
+ let app = test::init_service(
+ App::new()
+ .wrap(ErrorHandlers::new().default_handler(jsonify_plain_text_errors))
+ .route("/", web::get().to(service)),
+ )
+ .await;
+ let req = test::TestRequest::default().to_request();
+ let res = test::call_service(&app, req).await;
+
+ assert_eq!(res.status(), expected_status_code);
+
+ let body = test::read_body(res).await;
+ assert_eq!(body, expected_body);
+ }
+}
assert!(result.is_err());
assert!(
- result
- .unwrap_err()
- .error_type
- .eq(&Some(expected_err.clone())),
+ result.unwrap_err().error_type.eq(&expected_err.clone()),
"Testing {}, expected error {}",
invalid_name,
expected_err
&& invalid_result
.unwrap_err()
.error_type
- .eq(&Some(LemmyErrorType::BioLengthOverflow))
+ .eq(&LemmyErrorType::BioLengthOverflow)
);
}
&& invalid_result
.unwrap_err()
.error_type
- .eq(&Some(LemmyErrorType::SiteDescriptionLengthOverflow))
+ .eq(&LemmyErrorType::SiteDescriptionLengthOverflow)
);
}
assert!(result.is_err());
assert!(
- result
- .unwrap_err()
- .error_type
- .eq(&Some(expected_err.clone())),
+ result.unwrap_err().error_type.eq(&expected_err.clone()),
"Testing regex {:?}, expected error {}",
regex_str,
expected_err
use crate::{code_migrations::run_advanced_migrations, root_span_builder::QuieterRootSpanBuilder};
use activitypub_federation::config::{FederationConfig, FederationMiddleware};
use actix_cors::Cors;
-use actix_web::{middleware, web::Data, App, HttpServer, Result};
+use actix_web::{
+ middleware::{self, ErrorHandlers},
+ web::Data,
+ App,
+ HttpServer,
+ Result,
+};
use lemmy_api_common::{
context::LemmyContext,
lemmy_db_views::structs::SiteView,
use lemmy_utils::{
error::LemmyError,
rate_limit::RateLimitCell,
+ response::jsonify_plain_text_errors,
settings::SETTINGS,
SYNCHRONOUS_FEDERATION,
};
.wrap(middleware::Compress::default())
.wrap(cors_config)
.wrap(TracingLogger::<QuieterRootSpanBuilder>::new())
+ .wrap(ErrorHandlers::new().default_handler(jsonify_plain_text_errors))
.app_data(Data::new(context.clone()))
.app_data(Data::new(rate_limit_cell.clone()))
.wrap(FederationMiddleware::new(federation_config.clone()));