BUG: Multipart upload failure leaves dangling S3 parts and 'ingesting' asset row — no cleanup on error #85

Closed
opened 2026-05-25 18:46:35 -04:00 by zgaetano · 0 comments
Owner

Description

When a multipart upload fails mid-way (e.g., network disconnection during part upload), neither the frontend (screens-ingest.jsx) nor the server cleans up the dangling resources:

  1. The incomplete S3 multipart upload is never aborted
  2. The asset row remains in the DB with status = 'ingesting' — a zombie asset

Frontend (services/web-ui/public/screens-ingest.jsx):
The _uploadFile function's multipart branch creates the asset row and S3 multipart upload in the POST /upload/init step, then uploads parts in a loop. If the loop throws (e.g., part upload fails), control falls to the caller's catch handler in startUpload:

function startUpload(entry, pid) {
  _uploadFile(entry.file, pid, ...)
    .then(...)
    .catch(e => updateFile(entry.id, { status: 'error', progress: 0, error: e.message }));
}

The catch only updates the UI row — it never calls POST /upload/abort to clean up the dangling S3 parts or delete the asset row.

Backend (services/mam-api/src/routes/upload.js):
The POST /abort route exists and correctly aborts the S3 multipart upload and deletes the asset row. It's just never called on failure.

Suggested fix: In the _uploadFile function's multipart branch, wrap the part upload loop and complete call in a try/catch that calls POST /upload/abort before re-throwing the error. Something like:

let uploadId, key, assetId;
try {
  const init = await api.fetch('/upload/init', { ... });
  uploadId = init.uploadId; key = init.key; assetId = init.assetId;
  // ... upload parts ...
  await api.fetch('/upload/complete', { ... });
} catch (err) {
  if (uploadId && key && assetId) {
    api.fetch('/upload/abort', { method: 'POST', body: JSON.stringify({ uploadId, key, assetId }) })
      .catch(() => {});  // fire-and-forget cleanup
  }
  throw err;
}

(This is already partially tracked by existing issue #73 which covers BullMQ orphan jobs, but this is a different concern — S3 + DB cleanup vs. queue cleanup.)

## Description When a multipart upload fails mid-way (e.g., network disconnection during part upload), neither the frontend (`screens-ingest.jsx`) nor the server cleans up the dangling resources: 1. The incomplete S3 multipart upload is never aborted 2. The asset row remains in the DB with `status = 'ingesting'` — a zombie asset **Frontend (`services/web-ui/public/screens-ingest.jsx`):** The `_uploadFile` function's multipart branch creates the asset row and S3 multipart upload in the `POST /upload/init` step, then uploads parts in a loop. If the loop throws (e.g., part upload fails), control falls to the caller's `catch` handler in `startUpload`: ```js function startUpload(entry, pid) { _uploadFile(entry.file, pid, ...) .then(...) .catch(e => updateFile(entry.id, { status: 'error', progress: 0, error: e.message })); } ``` The `catch` only updates the UI row — it never calls `POST /upload/abort` to clean up the dangling S3 parts or delete the asset row. **Backend (`services/mam-api/src/routes/upload.js`):** The `POST /abort` route exists and correctly aborts the S3 multipart upload and deletes the asset row. It's just never called on failure. **Suggested fix:** In the `_uploadFile` function's multipart branch, wrap the part upload loop and complete call in a try/catch that calls `POST /upload/abort` before re-throwing the error. Something like: ```js let uploadId, key, assetId; try { const init = await api.fetch('/upload/init', { ... }); uploadId = init.uploadId; key = init.key; assetId = init.assetId; // ... upload parts ... await api.fetch('/upload/complete', { ... }); } catch (err) { if (uploadId && key && assetId) { api.fetch('/upload/abort', { method: 'POST', body: JSON.stringify({ uploadId, key, assetId }) }) .catch(() => {}); // fire-and-forget cleanup } throw err; } ``` (This is already partially tracked by existing issue #73 which covers BullMQ orphan jobs, but this is a different concern — S3 + DB cleanup vs. queue cleanup.)
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: WildDragonLLC/dragonflight#85
No description provided.