Fix #3366: Wrap plain-text error responses from the API in JSON (#3559)

* Fix #3366: API does return plain HTML errors

* Fix Clippy errors

* Improve api response times by doing send_activity asynchronously (#3493)

* do send_activity after http response

* move to util function

* format

* fix prometheus

* make synchronous federation configurable

* cargo fmt

* empty

* empty

---------

Co-authored-by: Dessalines <dessalines@users.noreply.github.com>

* Updating `login.rs` with generic `incorrect_login` response. (#3549)

* Adding v0.18.1 and v0.18.0 release notes. (#3530)

* Update RELEASES.md (#3556)

added instruction to find the location of your docker directory (especially useful for those who used ansible since they never had to setup docker manually)

* Use async email sender (#3554)

* Upgrade all dependencies (#3526)

* Upgrade all dependencies

* as base64

* Adding phiresky to codeowners. (#3576)

* Error enum fixed (#3487)

* Create error type enum

* Replace magic string slices with LemmyErrorTypes

* Remove unused enum

* Add rename snake case to error enum

* Rename functions

* clippy

* Fix merge errors

* Serialize in PascalCase instead of snake_case

* Revert src/lib

* Add serialization tests

* Update translations

* Fix compilation error in test

* Fix another compilation error

* Add code for generating typescript types

* Various fixes to avoid breaking api

* impl From<LemmyErrorType> for LemmyError

* with_lemmy_type

* trigger ci

---------

Co-authored-by: SleeplessOne1917 <abias1122@gmail.com>

* Only update site_aggregates for local site (#3516)

* Fix #3501 - Fix aggregation counts for elements removed and deleted (#3543)

Two bugs were found and fixed:
- previously elements removal and deletion were counted as two separate disappearances
- removing comments did not affect post aggregations

* Use LemmyErrorType also make error_type compulsory

* Add missing import for jsonify_plain_text_errors

* Fix formatting

* Trying to make woodpecker run again

---------

Co-authored-by: phiresky <phireskyde+git@gmail.com>
Co-authored-by: Dessalines <dessalines@users.noreply.github.com>
Co-authored-by: rosenjcb <rosenjcb@gmail.com>
Co-authored-by: nixoye <12674582+nixoye@users.noreply.github.com>
Co-authored-by: dullbananas <dull.bananas0@gmail.com>
Co-authored-by: Nutomic <me@nutomic.com>
Co-authored-by: SleeplessOne1917 <abias1122@gmail.com>
Co-authored-by: Sander Saarend <sander@saarend.com>
Co-authored-by: Piotr Juszczyk <74842304+pijuszczyk@users.noreply.github.com>
This commit is contained in:
Pawan Hegde 2023-07-11 02:14:14 +05:30 committed by GitHub
parent 9c2490d4f2
commit ef9dc5d0b6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 153 additions and 30 deletions

View file

@ -371,7 +371,7 @@ mod tests {
} }
Err(error) => { Err(error) => {
assert!( 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({})", "Got Err {:?}, but should have failed with message: {} for reason: {}. invalid_payloads.nth({})",
error.error_type, error.error_type,
expected_err, expected_err,

View file

@ -373,7 +373,7 @@ mod tests {
} }
Err(error) => { Err(error) => {
assert!( 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({})", "Got Err {:?}, but should have failed with message: {} for reason: {}. invalid_payloads.nth({})",
error.error_type, error.error_type,
expected_err, expected_err,

View file

@ -10,7 +10,7 @@ use ts_rs::TS;
pub type LemmyResult<T> = Result<T, LemmyError>; pub type LemmyResult<T> = Result<T, LemmyError>;
pub struct LemmyError { pub struct LemmyError {
pub error_type: Option<LemmyErrorType>, pub error_type: LemmyErrorType,
pub inner: anyhow::Error, pub inner: anyhow::Error,
pub context: SpanTrace, pub context: SpanTrace,
} }
@ -20,9 +20,10 @@ where
T: Into<anyhow::Error>, T: Into<anyhow::Error>,
{ {
fn from(t: T) -> Self { fn from(t: T) -> Self {
let cause = t.into();
LemmyError { LemmyError {
error_type: None, error_type: LemmyErrorType::Unknown(format!("{}", &cause)),
inner: t.into(), inner: cause,
context: SpanTrace::capture(), context: SpanTrace::capture(),
} }
} }
@ -40,9 +41,7 @@ impl Debug for LemmyError {
impl Display for LemmyError { impl Display for LemmyError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
if let Some(message) = &self.error_type { write!(f, "{}: ", &self.error_type)?;
write!(f, "{message}: ")?;
}
// print anyhow including trace // print anyhow including trace
// https://docs.rs/anyhow/latest/anyhow/struct.Error.html#display-representations // https://docs.rs/anyhow/latest/anyhow/struct.Error.html#display-representations
// this will print the anyhow trace (only if it exists) // this will print the anyhow trace (only if it exists)
@ -61,13 +60,7 @@ impl actix_web::error::ResponseError for LemmyError {
} }
fn error_response(&self) -> actix_web::HttpResponse { fn error_response(&self) -> actix_web::HttpResponse {
if let Some(message) = &self.error_type { actix_web::HttpResponse::build(self.status_code()).json(&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())
}
} }
} }
@ -214,14 +207,14 @@ pub enum LemmyErrorType {
CouldntCreateAudioCaptcha, CouldntCreateAudioCaptcha,
InvalidUrlScheme, InvalidUrlScheme,
CouldntSendWebmention, CouldntSendWebmention,
Unknown, Unknown(String),
} }
impl From<LemmyErrorType> for LemmyError { impl From<LemmyErrorType> for LemmyError {
fn from(error_type: LemmyErrorType) -> Self { fn from(error_type: LemmyErrorType) -> Self {
let inner = anyhow::anyhow!("{}", error_type); let inner = anyhow::anyhow!("{}", error_type);
LemmyError { LemmyError {
error_type: Some(error_type), error_type,
inner, inner,
context: SpanTrace::capture(), context: SpanTrace::capture(),
} }
@ -235,7 +228,7 @@ pub trait LemmyErrorExt<T, E: Into<anyhow::Error>> {
impl<T, E: Into<anyhow::Error>> LemmyErrorExt<T, E> for Result<T, E> { impl<T, E: Into<anyhow::Error>> LemmyErrorExt<T, E> for Result<T, E> {
fn with_lemmy_type(self, error_type: LemmyErrorType) -> Result<T, LemmyError> { fn with_lemmy_type(self, error_type: LemmyErrorType) -> Result<T, LemmyError> {
self.map_err(|error| LemmyError { self.map_err(|error| LemmyError {
error_type: Some(error_type), error_type,
inner: error.into(), inner: error.into(),
context: SpanTrace::capture(), context: SpanTrace::capture(),
}) })
@ -248,7 +241,7 @@ pub trait LemmyErrorExt2<T> {
impl<T> LemmyErrorExt2<T> for Result<T, LemmyError> { impl<T> LemmyErrorExt2<T> for Result<T, LemmyError> {
fn with_lemmy_type(self, error_type: LemmyErrorType) -> Result<T, LemmyError> { fn with_lemmy_type(self, error_type: LemmyErrorType) -> Result<T, LemmyError> {
self.map_err(|mut e| { self.map_err(|mut e| {
e.error_type = Some(error_type); e.error_type = error_type;
e e
}) })
} }

View file

@ -11,6 +11,7 @@ pub mod settings;
pub mod claims; pub mod claims;
pub mod error; pub mod error;
pub mod request; pub mod request;
pub mod response;
pub mod utils; pub mod utils;
pub mod version; pub mod version;

View file

@ -0,0 +1,127 @@
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);
}
}

View file

@ -431,10 +431,7 @@ mod tests {
assert!(result.is_err()); assert!(result.is_err());
assert!( assert!(
result result.unwrap_err().error_type.eq(&expected_err.clone()),
.unwrap_err()
.error_type
.eq(&Some(expected_err.clone())),
"Testing {}, expected error {}", "Testing {}, expected error {}",
invalid_name, invalid_name,
expected_err expected_err
@ -454,7 +451,7 @@ mod tests {
&& invalid_result && invalid_result
.unwrap_err() .unwrap_err()
.error_type .error_type
.eq(&Some(LemmyErrorType::BioLengthOverflow)) .eq(&LemmyErrorType::BioLengthOverflow)
); );
} }
@ -478,7 +475,7 @@ mod tests {
&& invalid_result && invalid_result
.unwrap_err() .unwrap_err()
.error_type .error_type
.eq(&Some(LemmyErrorType::SiteDescriptionLengthOverflow)) .eq(&LemmyErrorType::SiteDescriptionLengthOverflow)
); );
} }
@ -508,10 +505,7 @@ mod tests {
assert!(result.is_err()); assert!(result.is_err());
assert!( assert!(
result result.unwrap_err().error_type.eq(&expected_err.clone()),
.unwrap_err()
.error_type
.eq(&Some(expected_err.clone())),
"Testing regex {:?}, expected error {}", "Testing regex {:?}, expected error {}",
regex_str, regex_str,
expected_err expected_err

View file

@ -10,7 +10,13 @@ pub mod telemetry;
use crate::{code_migrations::run_advanced_migrations, root_span_builder::QuieterRootSpanBuilder}; use crate::{code_migrations::run_advanced_migrations, root_span_builder::QuieterRootSpanBuilder};
use activitypub_federation::config::{FederationConfig, FederationMiddleware}; use activitypub_federation::config::{FederationConfig, FederationMiddleware};
use actix_cors::Cors; 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::{ use lemmy_api_common::{
context::LemmyContext, context::LemmyContext,
lemmy_db_views::structs::SiteView, lemmy_db_views::structs::SiteView,
@ -29,6 +35,7 @@ use lemmy_routes::{feeds, images, nodeinfo, webfinger};
use lemmy_utils::{ use lemmy_utils::{
error::LemmyError, error::LemmyError,
rate_limit::RateLimitCell, rate_limit::RateLimitCell,
response::jsonify_plain_text_errors,
settings::SETTINGS, settings::SETTINGS,
SYNCHRONOUS_FEDERATION, SYNCHRONOUS_FEDERATION,
}; };
@ -181,6 +188,7 @@ pub async fn start_lemmy_server() -> Result<(), LemmyError> {
.wrap(middleware::Compress::default()) .wrap(middleware::Compress::default())
.wrap(cors_config) .wrap(cors_config)
.wrap(TracingLogger::<QuieterRootSpanBuilder>::new()) .wrap(TracingLogger::<QuieterRootSpanBuilder>::new())
.wrap(ErrorHandlers::new().default_handler(jsonify_plain_text_errors))
.app_data(Data::new(context.clone())) .app_data(Data::new(context.clone()))
.app_data(Data::new(rate_limit_cell.clone())) .app_data(Data::new(rate_limit_cell.clone()))
.wrap(FederationMiddleware::new(federation_config.clone())); .wrap(FederationMiddleware::new(federation_config.clone()));