error handling

This commit is contained in:
2025-10-29 11:14:19 +01:00
parent ac27a9ab36
commit ee8955574f
10 changed files with 102 additions and 36 deletions
+12 -3
View File
@@ -141,14 +141,20 @@ pub fn touch_webdav_token(conn: &mut PgPooledConnection, token_id: Uuid) -> Resu
pub fn verify_token_secret(secret: &str, token_hash: &str) -> Result<bool, AppError> { pub fn verify_token_secret(secret: &str, token_hash: &str) -> Result<bool, AppError> {
crate::auth::password::verify_password(secret, token_hash) crate::auth::password::verify_password(secret, token_hash)
.map_err(|err| AppError::internal(format!("failed to verify token: {err}"))) .map_err(|err| {
tracing::error!(error = ?err, "failed to verify token");
AppError::internal("failed to verify token")
})
} }
fn generate_secret() -> Result<String, AppError> { fn generate_secret() -> Result<String, AppError> {
let mut buffer = [0u8; TOKEN_SECRET_LENGTH]; let mut buffer = [0u8; TOKEN_SECRET_LENGTH];
OsRng OsRng
.try_fill_bytes(&mut buffer) .try_fill_bytes(&mut buffer)
.map_err(|err| AppError::internal(format!("failed to generate token: {err}")))?; .map_err(|err| {
tracing::error!(error = ?err, "failed to generate token");
AppError::internal("failed to generate token")
})?;
Ok(hex::encode(buffer)) Ok(hex::encode(buffer))
} }
@@ -156,7 +162,10 @@ fn hash_secret(secret: &str) -> Result<String, AppError> {
let salt = SaltString::generate(&mut OsRng); let salt = SaltString::generate(&mut OsRng);
let hash = Argon2::default() let hash = Argon2::default()
.hash_password(secret.as_bytes(), &salt) .hash_password(secret.as_bytes(), &salt)
.map_err(|err| AppError::internal(format!("failed to hash token: {err}")))?; .map_err(|err| {
tracing::error!(error = ?err, "failed to hash token");
AppError::internal("failed to hash token")
})?;
Ok(hash.to_string()) Ok(hash.to_string())
} }
+4 -1
View File
@@ -75,7 +75,10 @@ pub fn build_download_path(
.jwt .jwt
.generate_download_token(document.id, user_id, document.tenant_id) .generate_download_token(document.id, user_id, document.tenant_id)
.map(|token| format!("/download/{token}")) .map(|token| format!("/download/{token}"))
.map_err(|err| AppError::internal(format!("failed to generate download token: {err}"))) .map_err(|err| {
tracing::error!(error = ?err, "failed to generate download token");
AppError::internal("failed to generate download token")
})
} }
pub fn to_version_response(version: DocumentVersion) -> DocumentVersionResponse { pub fn to_version_response(version: DocumentVersion) -> DocumentVersionResponse {
+4 -1
View File
@@ -72,7 +72,10 @@ impl From<diesel::result::Error> for AppError {
fn from(value: diesel::result::Error) -> Self { fn from(value: diesel::result::Error) -> Self {
match value { match value {
diesel::result::Error::NotFound => AppError::not_found(), diesel::result::Error::NotFound => AppError::not_found(),
_ => AppError::internal(value), other => {
tracing::error!(error = ?other, "database operation failed");
AppError::internal("database operation failed")
}
} }
} }
} }
+16 -9
View File
@@ -47,6 +47,7 @@ use crate::schema::{
use crate::state::AppState; use crate::state::AppState;
use crate::utils::{ use crate::utils::{
db::{no_content, validate_bulk_ids, IntoJsonResponse}, db::{no_content, validate_bulk_ids, IntoJsonResponse},
error::StorageResultExt,
http::inline_content_disposition, http::inline_content_disposition,
json::{classify_nullable, NullableValue}, json::{classify_nullable, NullableValue},
storage_paths::document_version_object_key, storage_paths::document_version_object_key,
@@ -412,7 +413,10 @@ pub async fn list_documents(
let ids = quickwit_search(endpoint, index, tenant_id, query_str) let ids = quickwit_search(endpoint, index, tenant_id, query_str)
.await .await
.map_err(|err| AppError::internal(format!("quickwit search failed: {err}")))?; .map_err(|err| {
error!(error = ?err, "quickwit search failed");
AppError::internal("quickwit search failed")
})?;
if ids.is_empty() { if ids.is_empty() {
return Ok(Json(vec![])); return Ok(Json(vec![]));
@@ -913,7 +917,10 @@ pub async fn request_document_assets(
}), }),
None, None,
) )
.map_err(|err| AppError::internal(format!("failed to enqueue analyze job: {err}")))?; .map_err(|err| {
error!(error = ?err, "failed to enqueue analyze job");
AppError::internal("failed to enqueue analyze job")
})?;
Ok(StatusCode::ACCEPTED) Ok(StatusCode::ACCEPTED)
} }
@@ -959,7 +966,10 @@ pub async fn reanalyze_selected_documents(
}), }),
None, None,
) )
.map_err(|err| AppError::internal(format!("failed to enqueue analyze job: {err}")))?; .map_err(|err| {
error!(error = ?err, "failed to enqueue analyze job");
AppError::internal("failed to enqueue analyze job")
})?;
queued += 1; queued += 1;
} }
@@ -1048,7 +1058,7 @@ pub async fn get_document_asset(
Duration::from_secs(PRESIGNED_URL_EXPIRY_SECONDS), Duration::from_secs(PRESIGNED_URL_EXPIRY_SECONDS),
) )
.await .await
.map_err(|err| AppError::internal(format!("failed to generate asset URL: {err}")))?; .storage_context("failed to generate asset URL")?;
object_responses.push(to_asset_object_response( object_responses.push(to_asset_object_response(
object, object,
@@ -1178,7 +1188,7 @@ pub async fn download_with_token(
Duration::from_secs(PRESIGNED_URL_EXPIRY_SECONDS), Duration::from_secs(PRESIGNED_URL_EXPIRY_SECONDS),
) )
.await .await
.map_err(|err| AppError::internal(format!("failed to generate download URL: {err}")))?; .storage_context("failed to generate download URL")?;
Ok(axum::response::Redirect::temporary(&presigned_url)) Ok(axum::response::Redirect::temporary(&presigned_url))
} }
@@ -2035,10 +2045,7 @@ async fn process_upload(
content_disposition.clone(), content_disposition.clone(),
) )
.await .await
.map_err(|err| { .storage_context("failed to store document")?;
error!(error = %err, key = %s3_key, "failed to store document");
AppError::internal(format!("failed to store document: {err}"))
})?;
let metadata_value = if metadata.is_null() { let metadata_value = if metadata.is_null() {
Value::Object(Default::default()) Value::Object(Default::default())
+1 -1
View File
@@ -164,7 +164,7 @@ pub async fn ensure_folder_path(
last_folder = Some(folder); last_folder = Some(folder);
} }
last_folder.ok_or_else(|| AppError::internal("failed to resolve folder path".to_string())) last_folder.ok_or_else(|| AppError::internal("failed to resolve folder path"))
})?; })?;
Ok(Json(FolderResponse { Ok(Json(FolderResponse {
+20 -17
View File
@@ -25,7 +25,7 @@ use crate::schema::{
user_memberships::dsl as memberships_dsl, users::dsl as users_dsl, user_memberships::dsl as memberships_dsl, users::dsl as users_dsl,
}; };
use crate::state::AppState; use crate::state::AppState;
use crate::utils::{http::inline_content_disposition, time::to_http_date}; use crate::utils::{error::StorageResultExt, http::inline_content_disposition, time::to_http_date};
const REALM: &str = "Papercrate WebDAV"; const REALM: &str = "Papercrate WebDAV";
const DOWNLOAD_URL_TTL_SECONDS: u64 = 300; const DOWNLOAD_URL_TTL_SECONDS: u64 = 300;
@@ -108,8 +108,10 @@ async fn handle_propfind(
} }
}; };
let body = render_multistatus(&resources) let body = render_multistatus(&resources).map_err(|err| {
.map_err(|err| AppError::internal(format!("failed to render WebDAV response: {err}")))?; tracing::error!(error = ?err, "failed to render WebDAV response");
AppError::internal("failed to render WebDAV response")
})?;
let response = Response::builder() let response = Response::builder()
.status(multi_status()) .status(multi_status())
@@ -320,7 +322,7 @@ async fn stream_document(
Duration::from_secs(DOWNLOAD_URL_TTL_SECONDS), Duration::from_secs(DOWNLOAD_URL_TTL_SECONDS),
) )
.await .await
.map_err(|err| AppError::internal(format!("failed to presign document download: {err}")))?; .storage_context("failed to presign document download")?;
let client = reqwest::Client::new(); let client = reqwest::Client::new();
let mut request = client.request(method.clone(), url.clone()); let mut request = client.request(method.clone(), url.clone());
@@ -329,18 +331,17 @@ async fn stream_document(
request = request.header(header::RANGE, range.clone()); request = request.header(header::RANGE, range.clone());
} }
let upstream = request let upstream = request.send().await.map_err(|err| {
.send() tracing::error!(error = ?err, "failed to fetch document stream");
.await AppError::internal("failed to fetch document stream")
.map_err(|err| AppError::internal(format!("failed to fetch document stream: {err}")))?; })?;
let status = let status =
StatusCode::from_u16(upstream.status().as_u16()).unwrap_or(StatusCode::BAD_GATEWAY); StatusCode::from_u16(upstream.status().as_u16()).unwrap_or(StatusCode::BAD_GATEWAY);
if !(status.is_success() || status == StatusCode::PARTIAL_CONTENT) { if !(status.is_success() || status == StatusCode::PARTIAL_CONTENT) {
return Err(AppError::internal(format!( tracing::error!(status = %status, "upstream download returned error status");
"upstream download returned status {status}" return Err(AppError::internal("failed to fetch document stream"));
)));
} }
let mut builder = Response::builder().status(status); let mut builder = Response::builder().status(status);
@@ -368,9 +369,10 @@ async fn stream_document(
builder = builder.header(header::ETAG, format!("\"{}\"", version.id)); builder = builder.header(header::ETAG, format!("\"{}\"", version.id));
if method == Method::HEAD { if method == Method::HEAD {
return builder return builder.body(Body::empty()).map_err(|err| {
.body(Body::empty()) tracing::error!(error = ?err, "failed to build WebDAV response");
.map_err(|err| AppError::internal(format!("failed to build response: {err}"))); AppError::internal("failed to build WebDAV response")
});
} }
let stream = upstream let stream = upstream
@@ -378,9 +380,10 @@ async fn stream_document(
.map(|chunk| chunk.map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))); .map(|chunk| chunk.map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err)));
let body = Body::from_stream(stream); let body = Body::from_stream(stream);
builder builder.body(body).map_err(|err| {
.body(body) tracing::error!(error = ?err, "failed to build WebDAV response");
.map_err(|err| AppError::internal(format!("failed to build response: {err}"))) AppError::internal("failed to build WebDAV response")
})
} }
fn authenticate(state: &AppState, headers: &HeaderMap) -> Result<Option<WebDavContext>, AppError> { fn authenticate(state: &AppState, headers: &HeaderMap) -> Result<Option<WebDavContext>, AppError> {
+8 -3
View File
@@ -71,12 +71,17 @@ impl AppState {
pub(crate) fn db_unscoped(&self) -> AppResult<PgPooledConnection> { pub(crate) fn db_unscoped(&self) -> AppResult<PgPooledConnection> {
self.pool self.pool
.get() .get()
.map_err(|err| AppError::internal(format!("database pool error: {err}"))) .map_err(|err| {
tracing::error!(error = ?err, "database pool error");
AppError::internal("database pool error")
})
} }
pub fn storage_for_tenant(&self, tenant_id: Uuid) -> AppResult<TenantStorage> { pub fn storage_for_tenant(&self, tenant_id: Uuid) -> AppResult<TenantStorage> {
let tenant = self.tenants.get_by_id(tenant_id)?; let tenant = self.tenants.get_by_id(tenant_id)?;
TenantStorage::new(self.storage.clone(), &tenant) TenantStorage::new(self.storage.clone(), &tenant).map_err(|err| {
.map_err(|err| AppError::internal(format!("tenant storage error: {err}"))) tracing::error!(error = ?err, "tenant storage error");
AppError::internal("tenant storage error")
})
} }
} }
+4 -1
View File
@@ -57,7 +57,10 @@ impl TenantService {
let mut conn = self let mut conn = self
.pool .pool
.get() .get()
.map_err(|err| AppError::internal(format!("database pool error: {err}")))?; .map_err(|err| {
tracing::error!(error = ?err, "database pool error");
AppError::internal("database pool error")
})?;
let tenant = loader(&mut conn)?; let tenant = loader(&mut conn)?;
Ok(tenant) Ok(tenant)
} }
+32
View File
@@ -0,0 +1,32 @@
use diesel::result::Error as DieselError;
use crate::error::{AppError, AppResult};
pub trait DbResultExt<T> {
fn db_context(self, context: &'static str) -> AppResult<T>;
}
impl<T> DbResultExt<T> for Result<T, DieselError> {
fn db_context(self, context: &'static str) -> AppResult<T> {
self.map_err(|err| match err {
DieselError::NotFound => AppError::not_found(),
other => {
tracing::error!(error = ?other, "{context}");
AppError::internal(context)
}
})
}
}
pub trait StorageResultExt<T> {
fn storage_context(self, context: &'static str) -> AppResult<T>;
}
impl<T> StorageResultExt<T> for Result<T, anyhow::Error> {
fn storage_context(self, context: &'static str) -> AppResult<T> {
self.map_err(|err| {
tracing::error!(error = ?err, "{context}");
AppError::internal(context)
})
}
}
+1
View File
@@ -1,5 +1,6 @@
pub mod bootstrap; pub mod bootstrap;
pub mod db; pub mod db;
pub mod error;
pub mod http; pub mod http;
pub mod json; pub mod json;
pub mod storage_paths; pub mod storage_paths;