Skip to content

@tus/server: keep completed uploads queryable after expiration - #900

Open
ferhatelmas wants to merge 1 commit into
tus:mainfrom
ferhatelmas:head-expiration
Open

ferhatelmas wants to merge 1 commit into
tus:mainfrom
ferhatelmas:head-expiration

Conversation

@ferhatelmas

@ferhatelmas ferhatelmas commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

related to #898


Devin Review

Signed-off-by: Ferhat Elmas <elmas.ferhat@gmail.com>
@changeset-bot

changeset-bot Bot commented Oct 26, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 7c79672

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@tus/server Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 potential issues.

Devin Review

// be used if the Server is keeping track of expired uploads.
const now = new Date()
if (
file.offset !== file.size &&

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Expired S3 uploads falsely appear complete

After S3 cleanup aborts an expired upload, HeadHandler can return 200 and claim the upload completed. S3Store.getUpload treats a missing multipart upload with cached metadata as complete, even when cleanup deleted it.

Learn more

S3 uploads have an .info object describing their expected size and a multipart upload holding their actual parts. A HEAD request loads the .info object into the cache. When deleteExpired deletes that object and aborts the incomplete multipart upload, it leaves cached metadata behind. A later getUpload reads the cached record and treats NoSuchUpload as completion, setting the offset equal to the declared size. The new exemption then returns 200, although no completed object exists; this also occurs if an external lifecycle rule aborts the multipart upload but leaves its info object.

Example: Create a 10-byte S3 upload, HEAD it at offset 0 to warm the cache, wait for expiration, and call deleteExpired(). A subsequent HEAD returns Upload-Offset: 10 and status 200 instead of 410, although cleanup aborted its multipart upload.

Recommended fix: Distinguish an aborted upload from a completed upload in S3Store.getUpload before applying the HEAD exemption, and invalidate S3 metadata cache entries on cleanup. Check that the final object exists or use a persisted completion marker; a missing multipart upload alone cannot establish completion.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this needs fixing separately, #901 as a first step

// be used if the Server is keeping track of expired uploads.
const now = new Date()
if (
file.offset !== file.size &&

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Expired completion remains HEAD-only

A completed upload returns 200 to HEAD after expiration, while PatchHandler still rejects PATCH with 410. The changeset limits the exception to HEAD; confirm that this method distinction matches the intended client contract.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

intentional, patch after #888

This branch is waiting to be deployed

1 waiting deployment
external-testing — 7c79672d Waiting Oct 26, 2026 by ferhatelmas via Tests #118
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant