diff --git a/backend/src/error.rs b/backend/src/error.rs index 50898d9..62dfbfc 100644 --- a/backend/src/error.rs +++ b/backend/src/error.rs @@ -4,6 +4,7 @@ use axum::{ Json, }; use serde::Serialize; +use serde_json::Value; use std::fmt::Display; pub type AppResult = Result; @@ -13,6 +14,7 @@ pub struct AppError { status: StatusCode, message: String, code: Option, + details: Option, } impl AppError { @@ -21,6 +23,7 @@ impl AppError { status, message: message.into(), code: None, + details: None, } } @@ -48,6 +51,11 @@ impl AppError { self.code = Some(code.into()); self } + + pub fn with_details(mut self, details: Value) -> Self { + self.details = Some(details); + self + } } impl IntoResponse for AppError { @@ -56,6 +64,7 @@ impl IntoResponse for AppError { let body = Json(ErrorResponse { error: self.message, code: self.code, + details: self.details, }); (status, body).into_response() } @@ -66,6 +75,8 @@ struct ErrorResponse { error: String, #[serde(skip_serializing_if = "Option::is_none")] code: Option, + #[serde(skip_serializing_if = "Option::is_none")] + details: Option, } impl From for AppError { diff --git a/backend/src/routes/documents.rs b/backend/src/routes/documents.rs index 1525c37..d23bd3d 100644 --- a/backend/src/routes/documents.rs +++ b/backend/src/routes/documents.rs @@ -303,7 +303,6 @@ struct UploadRequest { enum UploadOutcome { Created(DocumentDetailResponse), Reused(DocumentDetailResponse), - Skipped { document_id: Uuid }, } #[derive(ToSchema)] @@ -322,7 +321,7 @@ pub struct UploadDocumentForm { pub correspondents: Option>, #[schema(nullable, example = "2024-01-01T00:00:00Z")] pub issued_at: Option, - #[schema(nullable)] + #[schema(nullable, default = true)] pub skip_existing: Option, } @@ -713,7 +712,7 @@ pub async fn get_document( responses( (status = 201, description = "Document created", body = DocumentDetailResponse), (status = 200, description = "Existing document reused", body = DocumentDetailResponse), - (status = 204, description = "Upload skipped because the document already exists") + (status = 409, description = "Document with identical contents already exists") ), tag = "Documents" )] @@ -732,7 +731,7 @@ pub async fn upload_document( let mut tag_ids: Vec = Vec::new(); let mut correspondents: Vec = Vec::new(); let mut issued_at_override: Option = None; - let mut skip_if_existing = false; + let mut skip_if_existing = true; let mut title_override: Option = None; while let Some(field) = multipart.next_field().await.map_err(|err| { @@ -913,10 +912,6 @@ pub async fn upload_document( ); (StatusCode::OK, Json(detail)).into_response() } - UploadOutcome::Skipped { document_id } => { - info!(document_id = %document_id, "document upload skipped by client request"); - StatusCode::NO_CONTENT.into_response() - } }; Ok(response) @@ -2138,11 +2133,15 @@ async fn process_upload( info!( document_id = %document.id, checksum = %checksum_hex, - "upload skipped existing document due to skip flag", + "upload rejected because document already exists", + ); + return Err( + AppError::conflict("a document with the same contents already exists") + .with_code("duplicate_document") + .with_details(json!({ + "conflict_document_id": document.id, + })), ); - return Ok(UploadOutcome::Skipped { - document_id: document.id, - }); } if let Some(issued_at) = issued_at_override {