Skip to main content

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}/download returns 500 with application/problem+json.
  • The response carries a backend x-request-id, and backend logs show FILE_501 or Storage operation failed.
  • The file appears in GET /v1/files and upload_completed is true.
  • Only one file fails, usually after a same-named file was uploaded and another same-named file was deleted.
  • MinIO returns NoSuchKey for the file_metadata.object_key.

Known Failure History

DateFailure modeDurable fix
2026-07-17Observed 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

SurfaceSource pathRequired behavior
Download entrylumie-backend/modules/file/src/main/java/com/lumie/file/adapter/in/web/FileController.javaGET /v1/files/{fileId}/download streams the authorized object and records download history for logged-in users.
Metadata lookuplumie-backend/modules/file/src/main/java/com/lumie/file/application/service/FileMetadataLookupService.javaResolve only completed, authorized FileMetadata rows before storage access.
Download servicelumie-backend/modules/file/src/main/java/com/lumie/file/application/service/FileQueryService.javaRead file_metadata.object_key and call StoragePort.getObject(...).
Storage adapterlumie-backend/modules/file/src/main/java/com/lumie/file/adapter/out/storage/MinioStorageAdapter.javaMap MinIO read failures to FILE_501.
Object key builderlumie-backend/modules/file/src/main/java/com/lumie/file/domain/vo/FilePath.javaBuild object keys as tenant/entityType/fileId/filename.
Collision testlumie-backend/modules/file/src/test/java/com/lumie/file/domain/vo/FilePathTest.javaProve 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

  1. Start with the x-request-id from the failing browser request.
  2. Search backend logs for that request id and confirm the failure is MinioStorageAdapter.getObject(...), not FileMetadataLookupService authorization.
  3. Read the affected row from file_metadata under the tenant context and capture id, entity_type, folder_id, original_filename, file_size, object_key, upload_completed, created_at, and updated_at.
  4. Search for other rows that shared the same legacy key shape or visible filename. This representative diagnostic query uses fields mapped by FileMetadata and created in V18__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;
  1. Confirm the object is missing in MinIO. In logs this appears as NoSuchKey; a versioned bucket may also show x-amz-delete-marker: true.
  2. Check whether a same-named file was deleted after the replacement upload. FileCommandService.deleteFile(...) removes the object by fileMetadata.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_filename to force uniqueness. Users may upload same-named files; the object key must carry uniqueness.
  • If the code path still builds tenant/entityType/filename, deploy the FilePath.toObjectKey() fix before accepting more same-named uploads.

Verification Evidence

  • The affected GET /v1/files/{fileId}/download returns 200 and streams the expected bytes.
  • Backend logs no longer show NoSuchKey for the affected key.
  • New same-named uploads produce different file_metadata.object_key values because the key includes fileId.
  • Deleting one same-named file does not remove another file's object.
  • ./gradlew :modules:file:test --tests com.lumie.file.domain.vo.FilePathTest passes in lumie-backend.

Prevention

  • Keep FilePath.toObjectKey() on the tenant/entityType/fileId/filename contract for both direct and presigned upload flows.
  • Keep FilePathTest or an equivalent collision test when refactoring the file module.
  • Treat object_key as the storage identity and original_filename as 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.