feat: better error handling

This commit is contained in:
Zomatree
2025-07-01 19:06:38 +01:00
parent ed22b3a5ce
commit 3d6f39a0eb
17 changed files with 336 additions and 55 deletions

View File

@@ -135,6 +135,7 @@ pub async fn web() -> Rocket<Build> {
.manage(cors.clone())
.attach(util::ratelimiter::RatelimitFairing)
.attach(cors)
.register("/", util::catchers::all_catchers())
.configure(rocket::Config {
limits: rocket::data::Limits::default().limit("string", 5.megabytes()),
address: Ipv4Addr::new(0, 0, 0, 0).into(),

View File

@@ -8,9 +8,9 @@ use revolt_models::v0;
use revolt_permissions::PermissionQuery;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result};
use rocket::serde::json::Json;
use crate::util::json::{Json, Validate};
use rocket::State;
use validator::Validate;
// use validator::Validate;
/// # Send Message
///
@@ -22,15 +22,10 @@ pub async fn message_send(
amqp: &State<AMQP>,
user: User,
target: Reference<'_>,
data: Json<v0::DataMessageSend>,
data: Validate<Json<v0::DataMessageSend>>,
idempotency: IdempotencyKey,
) -> Result<Json<v0::Message>> {
let data = data.into_inner();
data.validate().map_err(|error| {
create_error!(FailedValidation {
error: error.to_string()
})
})?;
// Ensure we have permissions to send a message
let channel = target.as_channel(db).await?;

View File

@@ -701,7 +701,7 @@ fn safe_from_str<T: for<'de> Deserialize<'de>>(data: &str) -> Result<T> {
match serde_json::from_str(data) {
Ok(output) => Ok(output),
Err(err) => {
log::error!("{err:?}");
revolt_config::capture_error(&err);
Err(create_error!(InvalidOperation))
}
}

View File

@@ -0,0 +1,19 @@
use rocket::{catch, Catcher, Request};
use revolt_result::{create_error, Error, Result};
#[catch(404)]
pub fn not_found() -> Result<()> {
Err(create_error!(NotFound))
}
#[catch(422)]
pub fn unprocessable_entity(req: &Request) -> Result<()> {
match req.local_cache(|| None::<Error>) {
Some(e) => Err(e.clone()),
None => Err(create_error!(UnprocessableEntity))
}
}
pub fn all_catchers() -> Vec<Catcher> {
catchers![not_found, unprocessable_entity]
}

View File

@@ -0,0 +1,176 @@
use std::fmt::Debug;
use revolt_rocket_okapi::{
r#gen::OpenApiGenerator,
response::OpenApiResponderInner,
revolt_okapi::openapi3::Responses,
util::add_schema_response,
request::OpenApiFromData,
revolt_okapi::{
openapi3::{MediaType, RequestBody},
Map,
},
};
use rocket::data::{Data, FromData, Limits, Outcome};
use rocket::response::{self, Responder, content};
use rocket::request::{local_cache, Request};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use revolt_result::{create_error, Error, ToRevoltError};
// A lot of this code is modified versions of rocket::serde::json so we can store
// the error so it can be passed to the error catcher.
macro_rules! fn_request_body {
($gen:ident, $ty:path, $mime_type:expr) => {{
let schema = $gen.json_schema::<$ty>();
Ok(RequestBody {
content: {
let mut map = Map::new();
map.insert(
$mime_type.to_owned(),
MediaType {
schema: Some(schema),
..MediaType::default()
},
);
map
},
required: true,
..revolt_rocket_okapi::revolt_okapi::openapi3::RequestBody::default()
})
}};
}
#[derive(Debug, Clone)]
pub struct Json<T>(pub T);
impl<'r, T: Deserialize<'r>> Json<T> {
#[inline]
pub fn into_inner(self) -> T {
self.0
}
fn from_str(s: &'r str) -> Result<Self, Error> {
serde_json::from_str(s)
.map(Json)
.map_err(|e| create_error!(DeserializationError { error: e.to_string() }))
}
async fn from_data(req: &'r Request<'_>, data: Data<'r>) -> Result<Self, Error> {
let limit = req.limits().get("json").unwrap_or(Limits::JSON);
let string = match data.open(limit).into_string().await {
Ok(s) if s.is_complete() => s.into_inner(),
Ok(_) => {
return Err(create_error!(PayloadTooLarge));
},
Err(_) => return Err(create_error!(IOError)),
};
Self::from_str(local_cache!(req, string))
}
}
impl<T> std::ops::Deref for Json<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[async_trait]
impl<'r, T: Deserialize<'r> + std::fmt::Debug> FromData<'r> for Json<T> {
type Error = Error;
async fn from_data(req: &'r Request<'_>, data: Data<'r>) -> Outcome<'r, Self> {
let r = Self::from_data(req, data).await;
match r {
Ok(value) => Outcome::Success(value),
Err(e) => {
req.local_cache(|| Some(e.clone()));
rocket::outcome::Outcome::Error((e.rocket_status(), e))
}
}
}
}
impl<'r, T: Serialize> Responder<'r, 'static> for Json<T> {
fn respond_to(self, req: &'r Request<'_>) -> response::Result<'static> {
match serde_json::to_string(&self.0).capture_error() {
Ok(string) => content::RawJson(string).respond_to(req),
Err(_) => create_error!(InternalError).respond_to(req)
}
}
}
impl<'r, T: JsonSchema + Deserialize<'r> + Debug> OpenApiFromData<'r> for Json<T> {
fn request_body(gen: &mut OpenApiGenerator) -> revolt_rocket_okapi::Result<RequestBody> {
fn_request_body!(gen, T, "application/json")
}
}
impl<T: Serialize + JsonSchema + Send> OpenApiResponderInner for Json<T> {
fn responses(gen: &mut OpenApiGenerator) -> revolt_rocket_okapi::Result<Responses> {
let mut responses = Responses::default();
let schema = gen.json_schema::<T>();
add_schema_response(&mut responses, 200, "application/json", schema)?;
Ok(responses)
}
}
impl<T: validator::Validate> validator::Validate for Json<T> {
fn validate(&self) -> Result<(), validator::ValidationErrors> {
self.0.validate()
}
}
pub struct Validate<T>(pub T);
impl<T> Validate<Json<T>> {
pub fn into_inner(self) -> T {
self.0.0
}
}
#[async_trait]
impl<'r, T: validator::Validate + Deserialize<'r> + Debug> FromData<'r> for Validate<Json<T>> {
type Error = Error;
async fn from_data(req: &'r Request<'_>, data: Data<'r>) -> Outcome<'r, Self> {
<Json::<T> as FromData<'r>>::from_data(req, data).await.and_then(|inner| {
if let Err(e) = inner.validate() {
let error = create_error!(FailedValidation { error: e.to_string() });
req.local_cache(|| Some(error.clone()));
return Outcome::Error((error.rocket_status(), error))
};
Outcome::Success(Self(inner))
})
}
}
impl<'r, T: JsonSchema + Deserialize<'r> + validator::Validate + Debug> OpenApiFromData<'r> for Validate<Json<T>> {
fn request_body(gen: &mut OpenApiGenerator) -> revolt_rocket_okapi::Result<RequestBody> {
fn_request_body!(gen, T, "application/json")
}
}
impl<T: Serialize + JsonSchema + Send> OpenApiResponderInner for Validate<Json<T>> {
fn responses(gen: &mut OpenApiGenerator) -> revolt_rocket_okapi::Result<Responses> {
let mut responses = Responses::default();
let schema = gen.json_schema::<T>();
add_schema_response(&mut responses, 200, "application/json", schema)?;
Ok(responses)
}
}
impl<T> std::ops::Deref for Validate<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}

View File

@@ -1,2 +1,4 @@
pub mod ratelimiter;
pub mod test;
pub mod catchers;
pub mod json;