backend 409 on document conflict

This commit is contained in:
2025-11-02 13:22:16 +01:00
parent 9ddaa59155
commit a30b6eaa02
2 changed files with 22 additions and 12 deletions
+11
View File
@@ -4,6 +4,7 @@ use axum::{
Json, Json,
}; };
use serde::Serialize; use serde::Serialize;
use serde_json::Value;
use std::fmt::Display; use std::fmt::Display;
pub type AppResult<T> = Result<T, AppError>; pub type AppResult<T> = Result<T, AppError>;
@@ -13,6 +14,7 @@ pub struct AppError {
status: StatusCode, status: StatusCode,
message: String, message: String,
code: Option<String>, code: Option<String>,
details: Option<Value>,
} }
impl AppError { impl AppError {
@@ -21,6 +23,7 @@ impl AppError {
status, status,
message: message.into(), message: message.into(),
code: None, code: None,
details: None,
} }
} }
@@ -48,6 +51,11 @@ impl AppError {
self.code = Some(code.into()); self.code = Some(code.into());
self self
} }
pub fn with_details(mut self, details: Value) -> Self {
self.details = Some(details);
self
}
} }
impl IntoResponse for AppError { impl IntoResponse for AppError {
@@ -56,6 +64,7 @@ impl IntoResponse for AppError {
let body = Json(ErrorResponse { let body = Json(ErrorResponse {
error: self.message, error: self.message,
code: self.code, code: self.code,
details: self.details,
}); });
(status, body).into_response() (status, body).into_response()
} }
@@ -66,6 +75,8 @@ struct ErrorResponse {
error: String, error: String,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
code: Option<String>, code: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
details: Option<Value>,
} }
impl From<diesel::result::Error> for AppError { impl From<diesel::result::Error> for AppError {
+11 -12
View File
@@ -303,7 +303,6 @@ struct UploadRequest {
enum UploadOutcome { enum UploadOutcome {
Created(DocumentDetailResponse), Created(DocumentDetailResponse),
Reused(DocumentDetailResponse), Reused(DocumentDetailResponse),
Skipped { document_id: Uuid },
} }
#[derive(ToSchema)] #[derive(ToSchema)]
@@ -322,7 +321,7 @@ pub struct UploadDocumentForm {
pub correspondents: Option<Vec<CorrespondentAssignmentInput>>, pub correspondents: Option<Vec<CorrespondentAssignmentInput>>,
#[schema(nullable, example = "2024-01-01T00:00:00Z")] #[schema(nullable, example = "2024-01-01T00:00:00Z")]
pub issued_at: Option<String>, pub issued_at: Option<String>,
#[schema(nullable)] #[schema(nullable, default = true)]
pub skip_existing: Option<bool>, pub skip_existing: Option<bool>,
} }
@@ -713,7 +712,7 @@ pub async fn get_document(
responses( responses(
(status = 201, description = "Document created", body = DocumentDetailResponse), (status = 201, description = "Document created", body = DocumentDetailResponse),
(status = 200, description = "Existing document reused", 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" tag = "Documents"
)] )]
@@ -732,7 +731,7 @@ pub async fn upload_document(
let mut tag_ids: Vec<Uuid> = Vec::new(); let mut tag_ids: Vec<Uuid> = Vec::new();
let mut correspondents: Vec<CorrespondentAssignmentInput> = Vec::new(); let mut correspondents: Vec<CorrespondentAssignmentInput> = Vec::new();
let mut issued_at_override: Option<NaiveDateTime> = None; let mut issued_at_override: Option<NaiveDateTime> = None;
let mut skip_if_existing = false; let mut skip_if_existing = true;
let mut title_override: Option<String> = None; let mut title_override: Option<String> = None;
while let Some(field) = multipart.next_field().await.map_err(|err| { 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() (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) Ok(response)
@@ -2138,11 +2133,15 @@ async fn process_upload(
info!( info!(
document_id = %document.id, document_id = %document.id,
checksum = %checksum_hex, 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 { if let Some(issued_at) = issued_at_override {