mirror of
https://github.com/modrinth/code.git
synced 2026-08-31 12:05:53 +00:00
refactor: labrinth ApiError and error reporting (#6981)
* refactor: labrinth `ApiError` and error reporting * fix clippy * fix ci
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
use crate::database::models::{DBUserId, users_compliance::FormType};
|
||||
use crate::env::ENV;
|
||||
use crate::routes::ApiError;
|
||||
use crate::util::error::ApiContext as _;
|
||||
use crate::util::error::Context as _;
|
||||
use ariadne::ids::base62_impl::to_base62;
|
||||
use chrono::Datelike;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -62,7 +64,8 @@ pub async fn request_form(
|
||||
}
|
||||
|
||||
let (request_builder, company_id) =
|
||||
team_request(reqwest::Method::POST, "/form_requests")?;
|
||||
team_request(reqwest::Method::POST, "/form_requests")
|
||||
.wrap_api_err("executing `team_request`")?;
|
||||
|
||||
let response = request_builder
|
||||
.json(&DataWrapper {
|
||||
@@ -84,12 +87,19 @@ pub async fn request_form(
|
||||
},
|
||||
})
|
||||
.send()
|
||||
.await?;
|
||||
.await
|
||||
.wrap_internal_err("deserializing HTTP response")?;
|
||||
|
||||
Ok(if response.status().is_success() {
|
||||
Ok(response.json::<DataWrapper<FormResponse>>().await?)
|
||||
Ok(response
|
||||
.json::<DataWrapper<FormResponse>>()
|
||||
.await
|
||||
.wrap_internal_err("deserializing HTTP response")?)
|
||||
} else {
|
||||
Err(response.json().await?)
|
||||
Err(response
|
||||
.json()
|
||||
.await
|
||||
.wrap_internal_err("deserializing HTTP response")?)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -104,12 +114,18 @@ pub async fn check_form(
|
||||
&format!(
|
||||
"/w9forms?filter[reference_id_eq]={reference_id}&page[number]=1&page[size]=1"
|
||||
),
|
||||
)?;
|
||||
).wrap_api_err("executing `team_request`")?;
|
||||
|
||||
let response = request_builder.send().await?;
|
||||
let response = request_builder
|
||||
.send()
|
||||
.await
|
||||
.wrap_internal_err("sending HTTP request")?;
|
||||
|
||||
Ok(if response.status().is_success() {
|
||||
let body = response.text().await?;
|
||||
let body = response
|
||||
.text()
|
||||
.await
|
||||
.wrap_internal_err("reading HTTP response body")?;
|
||||
let serde_result =
|
||||
serde_json::from_str::<ListWrapper<W9FormsResponse>>(&body);
|
||||
|
||||
@@ -118,13 +134,16 @@ pub async fn check_form(
|
||||
Ok(list_wrapper.data.pop().map(|data| DataWrapper { data }))
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(ApiError::InvalidInput(format!(
|
||||
return Err(ApiError::Request(eyre::eyre!(format!(
|
||||
"Error parsing avalara1099 response: {e}. Actual response body: {body}"
|
||||
)));
|
||||
))));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Err(response.json().await?)
|
||||
Err(response
|
||||
.json()
|
||||
.await
|
||||
.wrap_internal_err("deserializing HTTP response")?)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::env::ENV;
|
||||
use crate::routes::ApiError;
|
||||
use crate::util::error::Context as _;
|
||||
use actix_web::HttpRequest;
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
@@ -26,7 +27,7 @@ pub async fn check_hcaptcha(
|
||||
conn_info.peer_addr()
|
||||
};
|
||||
|
||||
let ip_addr = ip_addr.ok_or(ApiError::Turnstile)?;
|
||||
let ip_addr = ip_addr.wrap_request_err("captcha validation failed")?;
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
@@ -46,10 +47,11 @@ pub async fn check_hcaptcha(
|
||||
.form(&form)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|_| ApiError::Turnstile)?
|
||||
.wrap_request_err("captcha validation failed")?
|
||||
.json()
|
||||
.await
|
||||
.map_err(|_| ApiError::Turnstile)?;
|
||||
.map_err(|err| eyre::eyre!(err))
|
||||
.wrap_request_err("captcha validation failed")?;
|
||||
|
||||
Ok(val.success)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,31 @@ use std::{
|
||||
|
||||
use crate::routes::ApiError;
|
||||
|
||||
/// Adds context to an [`ApiError`] while preserving its HTTP status variant.
|
||||
pub trait ApiContext<T>: Sized {
|
||||
/// Wraps the report held by the error variant with a lazily-created message.
|
||||
fn wrap_api_err_with<D>(self, f: impl FnOnce() -> D) -> Result<T, ApiError>
|
||||
where
|
||||
D: Send + Sync + Debug + Display + 'static;
|
||||
|
||||
/// Wraps the report held by the error variant with the given message.
|
||||
fn wrap_api_err<D>(self, msg: D) -> Result<T, ApiError>
|
||||
where
|
||||
D: Send + Sync + Debug + Display + 'static,
|
||||
{
|
||||
self.wrap_api_err_with(|| msg)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ApiContext<T> for Result<T, ApiError> {
|
||||
fn wrap_api_err_with<D>(self, f: impl FnOnce() -> D) -> Result<T, ApiError>
|
||||
where
|
||||
D: Send + Sync + Debug + Display + 'static,
|
||||
{
|
||||
self.map_err(|error| error.wrap_err(f()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Allows wrapping [`Result`]s and [`Option`]s into [`Result<T, ApiError>`]s.
|
||||
#[allow(
|
||||
clippy::missing_errors_doc,
|
||||
@@ -85,6 +110,133 @@ pub trait Context<T, E>: Sized {
|
||||
{
|
||||
self.wrap_auth_err_with(|| msg)
|
||||
}
|
||||
|
||||
/// Maps the error variant into an [`ApiError::NotFound`] using the closure to create the message.
|
||||
#[inline]
|
||||
fn wrap_not_found_err_with<D>(
|
||||
self,
|
||||
f: impl FnOnce() -> D,
|
||||
) -> Result<T, ApiError>
|
||||
where
|
||||
D: Send + Sync + Debug + Display + 'static,
|
||||
{
|
||||
self.wrap_err_with(f).map_err(ApiError::NotFound)
|
||||
}
|
||||
|
||||
/// Maps the error variant into an [`ApiError::NotFound`] with the given message.
|
||||
#[inline]
|
||||
fn wrap_not_found_err<D>(self, msg: D) -> Result<T, ApiError>
|
||||
where
|
||||
D: Send + Sync + Debug + Display + 'static,
|
||||
{
|
||||
self.wrap_not_found_err_with(|| msg)
|
||||
}
|
||||
|
||||
/// Maps the error variant into an [`ApiError::Conflict`] using the closure to create the message.
|
||||
#[inline]
|
||||
fn wrap_conflict_err_with<D>(
|
||||
self,
|
||||
f: impl FnOnce() -> D,
|
||||
) -> Result<T, ApiError>
|
||||
where
|
||||
D: Send + Sync + Debug + Display + 'static,
|
||||
{
|
||||
self.wrap_err_with(f).map_err(ApiError::Conflict)
|
||||
}
|
||||
|
||||
/// Maps the error variant into an [`ApiError::Conflict`] with the given message.
|
||||
#[inline]
|
||||
fn wrap_conflict_err<D>(self, msg: D) -> Result<T, ApiError>
|
||||
where
|
||||
D: Send + Sync + Debug + Display + 'static,
|
||||
{
|
||||
self.wrap_conflict_err_with(|| msg)
|
||||
}
|
||||
|
||||
/// Maps the error variant into an [`ApiError::FailedDependency`] using the closure to create the message.
|
||||
#[inline]
|
||||
fn wrap_failed_dependency_err_with<D>(
|
||||
self,
|
||||
f: impl FnOnce() -> D,
|
||||
) -> Result<T, ApiError>
|
||||
where
|
||||
D: Send + Sync + Debug + Display + 'static,
|
||||
{
|
||||
self.wrap_err_with(f).map_err(ApiError::FailedDependency)
|
||||
}
|
||||
|
||||
/// Maps the error variant into an [`ApiError::FailedDependency`] with the given message.
|
||||
#[inline]
|
||||
fn wrap_failed_dependency_err<D>(self, msg: D) -> Result<T, ApiError>
|
||||
where
|
||||
D: Send + Sync + Debug + Display + 'static,
|
||||
{
|
||||
self.wrap_failed_dependency_err_with(|| msg)
|
||||
}
|
||||
|
||||
/// Maps the error variant into an [`ApiError::PreconditionRequired`] using the closure to create the message.
|
||||
#[inline]
|
||||
fn wrap_precondition_required_err_with<D>(
|
||||
self,
|
||||
f: impl FnOnce() -> D,
|
||||
) -> Result<T, ApiError>
|
||||
where
|
||||
D: Send + Sync + Debug + Display + 'static,
|
||||
{
|
||||
self.wrap_err_with(f)
|
||||
.map_err(ApiError::PreconditionRequired)
|
||||
}
|
||||
|
||||
/// Maps the error variant into an [`ApiError::PreconditionRequired`] with the given message.
|
||||
#[inline]
|
||||
fn wrap_precondition_required_err<D>(self, msg: D) -> Result<T, ApiError>
|
||||
where
|
||||
D: Send + Sync + Debug + Display + 'static,
|
||||
{
|
||||
self.wrap_precondition_required_err_with(|| msg)
|
||||
}
|
||||
|
||||
/// Maps the error variant into an [`ApiError::PreconditionFailed`] using the closure to create the message.
|
||||
#[inline]
|
||||
fn wrap_precondition_failed_err_with<D>(
|
||||
self,
|
||||
f: impl FnOnce() -> D,
|
||||
) -> Result<T, ApiError>
|
||||
where
|
||||
D: Send + Sync + Debug + Display + 'static,
|
||||
{
|
||||
self.wrap_err_with(f).map_err(ApiError::PreconditionFailed)
|
||||
}
|
||||
|
||||
/// Maps the error variant into an [`ApiError::PreconditionFailed`] with the given message.
|
||||
#[inline]
|
||||
fn wrap_precondition_failed_err<D>(self, msg: D) -> Result<T, ApiError>
|
||||
where
|
||||
D: Send + Sync + Debug + Display + 'static,
|
||||
{
|
||||
self.wrap_precondition_failed_err_with(|| msg)
|
||||
}
|
||||
|
||||
/// Maps the error variant into an [`ApiError::RateLimit`] using the closure to create the message.
|
||||
#[inline]
|
||||
fn wrap_rate_limit_err_with<D>(
|
||||
self,
|
||||
f: impl FnOnce() -> D,
|
||||
) -> Result<T, ApiError>
|
||||
where
|
||||
D: Send + Sync + Debug + Display + 'static,
|
||||
{
|
||||
self.wrap_err_with(f).map_err(ApiError::RateLimit)
|
||||
}
|
||||
|
||||
/// Maps the error variant into an [`ApiError::RateLimit`] with the given message.
|
||||
#[inline]
|
||||
fn wrap_rate_limit_err<D>(self, msg: D) -> Result<T, ApiError>
|
||||
where
|
||||
D: Send + Sync + Debug + Display + 'static,
|
||||
{
|
||||
self.wrap_rate_limit_err_with(|| msg)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, E> Context<T, E> for Result<T, E>
|
||||
@@ -146,6 +298,40 @@ mod tests {
|
||||
|
||||
let auth_error = ApiError::Auth(eyre::eyre!("auth error"));
|
||||
assert_eq!(auth_error.status_code(), StatusCode::UNAUTHORIZED);
|
||||
|
||||
let not_found_error =
|
||||
ApiError::NotFound(eyre::eyre!("not found error"));
|
||||
assert_eq!(not_found_error.status_code(), StatusCode::NOT_FOUND);
|
||||
|
||||
let conflict_error = ApiError::Conflict(eyre::eyre!("conflict error"));
|
||||
assert_eq!(conflict_error.status_code(), StatusCode::CONFLICT);
|
||||
|
||||
let dependency_error =
|
||||
ApiError::FailedDependency(eyre::eyre!("dependency error"));
|
||||
assert_eq!(
|
||||
dependency_error.status_code(),
|
||||
StatusCode::FAILED_DEPENDENCY
|
||||
);
|
||||
|
||||
let required_error = ApiError::PreconditionRequired(eyre::eyre!(
|
||||
"precondition required error"
|
||||
));
|
||||
assert_eq!(
|
||||
required_error.status_code(),
|
||||
StatusCode::PRECONDITION_REQUIRED
|
||||
);
|
||||
|
||||
let failed_error = ApiError::PreconditionFailed(eyre::eyre!(
|
||||
"precondition failed error"
|
||||
));
|
||||
assert_eq!(failed_error.status_code(), StatusCode::PRECONDITION_FAILED);
|
||||
|
||||
let rate_limit_error =
|
||||
ApiError::RateLimit(eyre::eyre!("rate limit error"));
|
||||
assert_eq!(
|
||||
rate_limit_error.status_code(),
|
||||
StatusCode::TOO_MANY_REQUESTS
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -237,6 +423,68 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_context_trait_status_errors() {
|
||||
let not_found: Option<i32> = None;
|
||||
assert!(matches!(
|
||||
not_found.wrap_not_found_err("missing value").unwrap_err(),
|
||||
ApiError::NotFound(_)
|
||||
));
|
||||
|
||||
let conflict: Option<i32> = None;
|
||||
assert!(matches!(
|
||||
conflict.wrap_conflict_err("conflicting value").unwrap_err(),
|
||||
ApiError::Conflict(_)
|
||||
));
|
||||
|
||||
let dependency: Option<i32> = None;
|
||||
assert!(matches!(
|
||||
dependency
|
||||
.wrap_failed_dependency_err("dependency failed")
|
||||
.unwrap_err(),
|
||||
ApiError::FailedDependency(_)
|
||||
));
|
||||
|
||||
let required: Option<i32> = None;
|
||||
assert!(matches!(
|
||||
required
|
||||
.wrap_precondition_required_err("precondition required")
|
||||
.unwrap_err(),
|
||||
ApiError::PreconditionRequired(_)
|
||||
));
|
||||
|
||||
let failed: Option<i32> = None;
|
||||
assert!(matches!(
|
||||
failed
|
||||
.wrap_precondition_failed_err("precondition failed")
|
||||
.unwrap_err(),
|
||||
ApiError::PreconditionFailed(_)
|
||||
));
|
||||
|
||||
let rate_limit: Option<i32> = None;
|
||||
assert!(matches!(
|
||||
rate_limit
|
||||
.wrap_rate_limit_err("rate limit exceeded")
|
||||
.unwrap_err(),
|
||||
ApiError::RateLimit(_)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_api_context_preserves_status_variant() {
|
||||
let result: Result<(), ApiError> =
|
||||
Err(ApiError::NotFound(eyre::eyre!("missing value")));
|
||||
let error = result.wrap_api_err("fetching test value").unwrap_err();
|
||||
|
||||
match error {
|
||||
ApiError::NotFound(report) => {
|
||||
assert_eq!(report.to_string(), "fetching test value");
|
||||
assert!(format!("{report:#}").contains("missing value"));
|
||||
}
|
||||
_ => panic!("expected NotFound error"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_context_trait_with_closure() {
|
||||
let result: Result<i32, std::io::Error> = Err(std::io::Error::new(
|
||||
|
||||
@@ -2,6 +2,7 @@ use crate::env::ENV;
|
||||
use crate::models::ids::PayoutId;
|
||||
use crate::routes::ApiError;
|
||||
use crate::routes::internal::gotenberg::{GotenbergDocument, GotenbergError};
|
||||
use crate::util::error::ApiContext as _;
|
||||
use crate::util::error::Context;
|
||||
use actix_web::http::header::HeaderName;
|
||||
use chrono::{DateTime, Datelike, Utc};
|
||||
@@ -166,7 +167,7 @@ impl GotenbergClient {
|
||||
.await
|
||||
.wrap_internal_err("failed to submit HTML to Gotenberg")?
|
||||
.error_for_status()
|
||||
.wrap_internal_err("Gotenberg returned an error status")?;
|
||||
.wrap_internal_err("received an error status from Gotenberg")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -190,7 +191,9 @@ impl GotenbergClient {
|
||||
&self,
|
||||
statement: &PaymentStatement,
|
||||
) -> Result<GotenbergDocument, ApiError> {
|
||||
self.generate_payment_statement(statement).await?;
|
||||
self.generate_payment_statement(statement)
|
||||
.await
|
||||
.wrap_api_err("executing `generate_payment_statement`")?;
|
||||
|
||||
let timeout_ms = ENV.GOTENBERG_TIMEOUT;
|
||||
let redis_timeout_ms =
|
||||
@@ -205,7 +208,7 @@ impl GotenbergClient {
|
||||
.brpop(&response_key, Duration::from_millis(redis_timeout_ms)),
|
||||
)
|
||||
.await
|
||||
.wrap_internal_err("Gotenberg document generation timed out")?
|
||||
.wrap_internal_err("timed out generating Gotenberg document")?
|
||||
.wrap_internal_err("failed to get document over Redis")?
|
||||
.wrap_internal_err("no document was returned from Redis")?;
|
||||
|
||||
@@ -213,7 +216,7 @@ impl GotenbergClient {
|
||||
Result<GotenbergDocument, GotenbergError>,
|
||||
>(&document)
|
||||
.wrap_internal_err("failed to deserialize Redis document response")?
|
||||
.wrap_internal_err("Gotenberg document generation failed")?;
|
||||
.wrap_internal_err("failed to generate Gotenberg document")?;
|
||||
|
||||
Ok(document)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ use crate::env::ENV;
|
||||
use crate::file_hosting::{FileHost, FileHostPublicity};
|
||||
use crate::models::images::ImageContext;
|
||||
use crate::routes::ApiError;
|
||||
use crate::util::error::Context as _;
|
||||
use color_thief::ColorFormat;
|
||||
use hex::ToHex;
|
||||
use image::imageops::FilterType;
|
||||
@@ -54,10 +55,8 @@ pub async fn upload_image_optimized(
|
||||
file_host: &dyn FileHost,
|
||||
) -> Result<UploadImageResult, ApiError> {
|
||||
let content_type = crate::util::ext::get_image_content_type(file_extension)
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(format!(
|
||||
"Invalid format for image: {file_extension}"
|
||||
))
|
||||
.wrap_request_err_with(|| {
|
||||
format!("invalid format for image: {file_extension}")
|
||||
})?;
|
||||
|
||||
let cdn_url = &ENV.CDN_URL;
|
||||
@@ -68,8 +67,10 @@ pub async fn upload_image_optimized(
|
||||
content_type,
|
||||
target_width,
|
||||
min_aspect_ratio,
|
||||
)?;
|
||||
let color = get_color_from_img(&bytes)?;
|
||||
)
|
||||
.wrap_request_err("processing uploaded image")?;
|
||||
let color = get_color_from_img(&bytes)
|
||||
.wrap_request_err("extracting color from uploaded image")?;
|
||||
|
||||
// Only upload the processed image if it's smaller than the original
|
||||
let processed_upload_data = if processed_image.len() < bytes.len() {
|
||||
@@ -87,7 +88,8 @@ pub async fn upload_image_optimized(
|
||||
publicity,
|
||||
processed_image,
|
||||
)
|
||||
.await?,
|
||||
.await
|
||||
.wrap_internal_err("uploading file to file host")?,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
@@ -100,7 +102,8 @@ pub async fn upload_image_optimized(
|
||||
publicity,
|
||||
bytes,
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.wrap_internal_err("uploading file to file host")?;
|
||||
|
||||
let url = format!("{}/{}", cdn_url, upload_data.file_name);
|
||||
Ok(UploadImageResult {
|
||||
@@ -182,7 +185,10 @@ pub async fn delete_old_images(
|
||||
let name = image_url.split(&cdn_url_start).nth(1);
|
||||
|
||||
if let Some(icon_path) = name {
|
||||
file_host.delete_file(icon_path, publicity).await?;
|
||||
file_host
|
||||
.delete_file(icon_path, publicity)
|
||||
.await
|
||||
.wrap_internal_err("deleting file from file host")?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,7 +196,10 @@ pub async fn delete_old_images(
|
||||
let name = raw_image_url.split(&cdn_url_start).nth(1);
|
||||
|
||||
if let Some(icon_path) = name {
|
||||
file_host.delete_file(icon_path, publicity).await?;
|
||||
file_host
|
||||
.delete_file(icon_path, publicity)
|
||||
.await
|
||||
.wrap_internal_err("deleting file from file host")?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,7 +217,8 @@ pub async fn delete_unused_images(
|
||||
) -> Result<(), ApiError> {
|
||||
let uploaded_images =
|
||||
database::models::DBImage::get_many_contexted(context, transaction)
|
||||
.await?;
|
||||
.await
|
||||
.wrap_internal_err("fetching images from database")?;
|
||||
|
||||
for image in uploaded_images {
|
||||
let mut should_delete = true;
|
||||
@@ -220,8 +230,12 @@ pub async fn delete_unused_images(
|
||||
}
|
||||
|
||||
if should_delete {
|
||||
image_item::DBImage::remove(image.id, transaction, redis).await?;
|
||||
image_item::DBImage::clear_cache(image.id, redis).await?;
|
||||
image_item::DBImage::remove(image.id, transaction, redis)
|
||||
.await
|
||||
.wrap_internal_err("deleting image from database")?;
|
||||
image_item::DBImage::clear_cache(image.id, redis)
|
||||
.await
|
||||
.wrap_internal_err("clearing cached data from Redis")?;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -178,10 +178,10 @@ pub async fn rate_limit_middleware(
|
||||
|
||||
Ok(service_response.map_into_left_body())
|
||||
} else {
|
||||
let mut response = ApiError::RateLimitError(
|
||||
decision.retry_after_ms.unwrap_or(0) as u128,
|
||||
decision.limit,
|
||||
)
|
||||
let retry_after_ms = decision.retry_after_ms.unwrap_or(0);
|
||||
let mut response = ApiError::RateLimit(eyre::eyre!(
|
||||
"rate limit exceeded; retry after {retry_after_ms} milliseconds"
|
||||
))
|
||||
.error_response();
|
||||
|
||||
// Add rate limit headers
|
||||
@@ -220,10 +220,9 @@ pub async fn rate_limit_middleware(
|
||||
Ok(req.into_response(response.map_into_right_body()))
|
||||
}
|
||||
} else {
|
||||
let response = ApiError::CustomAuthentication(
|
||||
"Unable to obtain user IP address!".to_string(),
|
||||
)
|
||||
.error_response();
|
||||
let response =
|
||||
ApiError::Auth(eyre::eyre!("Unable to obtain user IP address!",))
|
||||
.error_response();
|
||||
|
||||
Ok(req.into_response(response.map_into_right_body()))
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::routes::ApiError;
|
||||
use crate::routes::v3::project_creation::CreateError;
|
||||
use crate::util::validate::validation_errors_to_string;
|
||||
use crate::util::error::Context as _;
|
||||
use actix_multipart::Field;
|
||||
use actix_web::web::Payload;
|
||||
use bytes::BytesMut;
|
||||
@@ -16,13 +16,13 @@ pub async fn read_limited_from_payload(
|
||||
let mut bytes = BytesMut::new();
|
||||
while let Some(item) = payload.next().await {
|
||||
if bytes.len() >= cap {
|
||||
return Err(ApiError::InvalidInput(String::from(err_msg)));
|
||||
return Err(ApiError::Request(eyre::eyre!(String::from(err_msg))));
|
||||
} else {
|
||||
bytes.extend_from_slice(&item.map_err(|_| {
|
||||
ApiError::InvalidInput(
|
||||
"Unable to parse bytes in payload sent!".to_string(),
|
||||
)
|
||||
})?);
|
||||
bytes.extend_from_slice(
|
||||
&item.map_err(|err| eyre::eyre!(err)).wrap_request_err(
|
||||
"unable to parse bytes in payload sent!".to_string(),
|
||||
)?,
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(bytes)
|
||||
@@ -36,17 +36,19 @@ where
|
||||
{
|
||||
let mut bytes = BytesMut::new();
|
||||
while let Some(item) = payload.next().await {
|
||||
bytes.extend_from_slice(&item.map_err(|_| {
|
||||
ApiError::InvalidInput(
|
||||
"Unable to parse bytes in payload sent!".to_string(),
|
||||
)
|
||||
})?);
|
||||
bytes.extend_from_slice(
|
||||
&item.map_err(|err| eyre::eyre!(err)).wrap_request_err(
|
||||
"unable to parse bytes in payload sent!".to_string(),
|
||||
)?,
|
||||
);
|
||||
}
|
||||
|
||||
let parsed: T = serde_json::from_slice(&bytes)?;
|
||||
parsed.validate().map_err(|err| {
|
||||
ApiError::InvalidInput(validation_errors_to_string(err, None))
|
||||
})?;
|
||||
let parsed: T = serde_json::from_slice(&bytes)
|
||||
.wrap_request_err("deserializing JSON data")?;
|
||||
parsed
|
||||
.validate()
|
||||
.map_err(|err| eyre::eyre!(err))
|
||||
.wrap_request_err("validating request")?;
|
||||
Ok(parsed)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use crate::database::models::legacy_loader_fields::MinecraftGameVersion;
|
||||
use crate::models::ids::ProjectId;
|
||||
use crate::routes::ApiError;
|
||||
use crate::util::error::ApiContext as _;
|
||||
use crate::util::error::Context as _;
|
||||
use crate::{database::PgPool, env::ENV};
|
||||
use ariadne::ids::base62_impl::to_base62;
|
||||
use chrono::{DateTime, Utc};
|
||||
@@ -51,7 +53,8 @@ async fn get_webhook_metadata(
|
||||
pool,
|
||||
redis,
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.wrap_api_err("fetching webhook project")?;
|
||||
|
||||
if let Some(mut project) = project {
|
||||
let mut owner = None;
|
||||
@@ -62,7 +65,7 @@ async fn get_webhook_metadata(
|
||||
pool,
|
||||
redis,
|
||||
)
|
||||
.await?;
|
||||
.await.wrap_internal_err("fetching organization from database")?;
|
||||
|
||||
if let Some(organization) = organization {
|
||||
owner = Some(WebhookAuthor {
|
||||
@@ -81,7 +84,7 @@ async fn get_webhook_metadata(
|
||||
pool,
|
||||
redis,
|
||||
)
|
||||
.await?;
|
||||
.await.wrap_internal_err("fetching team member from database")?;
|
||||
|
||||
if let Some(member) = team.into_iter().find(|x| x.is_owner) {
|
||||
let user = crate::database::models::user_item::DBUser::get_id(
|
||||
@@ -89,7 +92,8 @@ async fn get_webhook_metadata(
|
||||
pool,
|
||||
redis,
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.wrap_internal_err("fetching user from database")?;
|
||||
|
||||
if let Some(user) = user {
|
||||
owner = Some(WebhookAuthor {
|
||||
@@ -106,7 +110,11 @@ async fn get_webhook_metadata(
|
||||
};
|
||||
|
||||
let all_game_versions =
|
||||
MinecraftGameVersion::list(None, None, pool, redis).await?;
|
||||
MinecraftGameVersion::list(None, None, pool, redis)
|
||||
.await
|
||||
.wrap_internal_err(
|
||||
"fetching minecraft game version from Redis",
|
||||
)?;
|
||||
|
||||
let versions = project
|
||||
.aggregate_version_fields
|
||||
@@ -249,9 +257,7 @@ pub async fn send_slack_payout_source_alert_webhook(
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|_| {
|
||||
ApiError::Slack("Error while sending projects webhook".to_string())
|
||||
})?;
|
||||
.map_err(|err| eyre::eyre!(err)).wrap_internal_err("error while sending projects webhook".to_string())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -263,7 +269,9 @@ pub async fn send_slack_project_webhook(
|
||||
webhook_url: &str,
|
||||
message: Option<String>,
|
||||
) -> Result<(), ApiError> {
|
||||
let metadata = get_webhook_metadata(project_id, pool, redis).await?;
|
||||
let metadata = get_webhook_metadata(project_id, pool, redis)
|
||||
.await
|
||||
.wrap_api_err("fetching webhook metadata")?;
|
||||
|
||||
if let Some(metadata) = metadata {
|
||||
let mut blocks = vec![];
|
||||
@@ -365,11 +373,10 @@ pub async fn send_slack_project_webhook(
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|_| {
|
||||
ApiError::Slack(
|
||||
"Error while sending projects webhook".to_string(),
|
||||
)
|
||||
})?;
|
||||
.map_err(|err| eyre::eyre!(err))
|
||||
.wrap_internal_err(
|
||||
"error while sending projects webhook".to_string(),
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -434,7 +441,9 @@ pub async fn send_discord_webhook(
|
||||
webhook_url: &str,
|
||||
message: Option<String>,
|
||||
) -> Result<(), ApiError> {
|
||||
let metadata = get_webhook_metadata(project_id, pool, redis).await?;
|
||||
let metadata = get_webhook_metadata(project_id, pool, redis)
|
||||
.await
|
||||
.wrap_api_err("fetching webhook metadata")?;
|
||||
|
||||
if let Some(project) = metadata {
|
||||
let mut fields = vec![];
|
||||
@@ -503,11 +512,10 @@ pub async fn send_discord_webhook(
|
||||
})
|
||||
.send()
|
||||
.await
|
||||
.map_err(|_| {
|
||||
ApiError::Discord(
|
||||
"Error while sending projects webhook".to_string(),
|
||||
)
|
||||
})?;
|
||||
.map_err(|err| eyre::eyre!(err))
|
||||
.wrap_failed_dependency_err(
|
||||
"error while sending projects webhook".to_string(),
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
Reference in New Issue
Block a user