File Download Object Key Collision
Use this guide when a single file download fails with 500 while the file metadata row still exists and nearby file downloads work.
This failure sits in the file module's backend-to-object-storage boundary. It is different from authorization failures, which should return 403 or 404 before MinIO is called.
Symptoms
GET /v1/files/{fileId}/downloadreturns500withapplication/problem+json.- The response carries a backend
x-request-id, and backend logs showFILE_501orStorage operation failed. - The file appears in
GET /v1/filesandupload_completedistrue. - Only one file fails, usually after a same-named file was uploaded and another same-named file was deleted.
- MinIO returns
NoSuchKeyfor thefile_metadata.object_key.
Known Failure History
| Date | Failure mode | Durable fix |
|---|---|---|
| 2026-07-17 | Observed production example: joossameng textbook file exam_review.pdf downloaded through metadata id 10e28afa-87c1-47ee-847d-8940d85058ff, but MinIO returned NoSuchKey for joossameng/textbook/exam_review.pdf. The investigation found a same-named older file had been deleted after the new upload, deleting the shared object key. | FilePath.toObjectKey() now includes fileId, so uploads with the same visible filename produce distinct object keys: tenant/entityType/fileId/filename. |
Current Runtime Contract
| Surface | Source path | Required behavior |
|---|---|---|
| Download entry | lumie-backend/modules/file/src/main/java/com/lumie/file/adapter/in/web/FileController.java | GET /v1/files/{fileId}/download streams the authorized object and records download history for logged-in users. |
| Metadata lookup | lumie-backend/modules/file/src/main/java/com/lumie/file/application/service/FileMetadataLookupService.java | Resolve only completed, authorized FileMetadata rows before storage access. |
| Download service | lumie-backend/modules/file/src/main/java/com/lumie/file/application/service/FileQueryService.java | Read file_metadata.object_key and call StoragePort.getObject(...). |
| Storage adapter | lumie-backend/modules/file/src/main/java/com/lumie/file/adapter/out/storage/MinioStorageAdapter.java | Map MinIO read failures to FILE_501. |
| Object key builder | lumie-backend/modules/file/src/main/java/com/lumie/file/domain/vo/FilePath.java | Build object keys as tenant/entityType/fileId/filename. |
| Collision test | lumie-backend/modules/file/src/test/java/com/lumie/file/domain/vo/FilePathTest.java | Prove two exam_review.pdf uploads under the same tenant and entity type produce different object keys. |
The visible filename remains FileMetadata.originalFilename. Do not make the display name globally unique just to protect storage. Storage uniqueness belongs in object_key.
Diagnosis
- Start with the
x-request-idfrom the failing browser request. - Search backend logs for that request id and confirm the failure is
MinioStorageAdapter.getObject(...), notFileMetadataLookupServiceauthorization. - Read the affected row from
file_metadataunder the tenant context and captureid,entity_type,folder_id,original_filename,file_size,object_key,upload_completed,created_at, andupdated_at. - Search for other rows that shared the same legacy key shape or visible filename. This representative diagnostic query uses fields mapped by
FileMetadataand created inV18__rls_baseline.sql:
SET app.tenant_id = '<tenant-id>';
SELECT id, entity_type, folder_id, original_filename, file_size,
object_key, upload_completed, created_at, updated_at
FROM file_metadata
WHERE object_key = '<object-key>'
OR original_filename = '<filename>'
ORDER BY created_at;
- Confirm the object is missing in MinIO. In logs this appears as
NoSuchKey; a versioned bucket may also showx-amz-delete-marker: true. - Check whether a same-named file was deleted after the replacement upload.
FileCommandService.deleteFile(...)removes the object byfileMetadata.getObjectKey(), so duplicate legacy keys let an old metadata delete remove the new physical object.
The 2026-07-17 observed example had this runtime log signature:
Failed to get object: joossameng/textbook/exam_review.pdf
ErrorResponse(code = NoSuchKey, message = The specified key does not exist.)
x-amz-delete-marker: true
Business exception [FILE_501]: Storage operation failed
Fix
- If the missing object has a recoverable MinIO version, restore the object version for the affected
object_key. - If no object version is recoverable, ask the tenant to reupload the file and remove or replace the broken metadata row through the normal product flow.
- Do not repair by pointing multiple metadata rows at the same object key.
- Do not change
original_filenameto force uniqueness. Users may upload same-named files; the object key must carry uniqueness. - If the code path still builds
tenant/entityType/filename, deploy theFilePath.toObjectKey()fix before accepting more same-named uploads.
Verification Evidence
- The affected
GET /v1/files/{fileId}/downloadreturns200and streams the expected bytes. - Backend logs no longer show
NoSuchKeyfor the affected key. - New same-named uploads produce different
file_metadata.object_keyvalues because the key includesfileId. - Deleting one same-named file does not remove another file's object.
./gradlew :modules:file:test --tests com.lumie.file.domain.vo.FilePathTestpasses inlumie-backend.
Prevention
- Keep
FilePath.toObjectKey()on thetenant/entityType/fileId/filenamecontract for both direct and presigned upload flows. - Keep
FilePathTestor an equivalent collision test when refactoring the file module. - Treat
object_keyas the storage identity andoriginal_filenameas display metadata. - When investigating file drift, compare database metadata and MinIO object existence before changing authorization or folder queries.
- Prefer fixing storage identity at the object-key builder instead of adding filename restrictions to the UI.