Skip to content

@tus/s3-store: complete empty multipart uploads on create - #876

Open
hsusul wants to merge 3 commits into
tus:mainfrom
hsusul:fix/s3-store-zero-byte-create
Open

@tus/s3-store: complete empty multipart uploads on create#876
hsusul wants to merge 3 commits into
tus:mainfrom
hsusul:fix/s3-store-zero-byte-create

Conversation

@hsusul

@hsusul hsusul commented Sep 4, 2026

Copy link
Copy Markdown

Problem

@tus/server treats Upload-Length: 0 as immediately final and calls onUploadFinish without write() (PostHandler sets isFinal when size === 0 && !sizeIsDeferred).

S3Store.create only starts a multipart upload and writes the .info object. Consumers that HeadObject / getObject the key in onUploadFinish (e.g. Supabase Storage) then see a bare 404 because the multipart was never completed.

finishMultipartUpload already handles empty parts by uploading a zero-byte part — it just was not reached on the create-only path.

Reported downstream: supabase/storage#1300
(Supabase maintainer asked to fix upstream: supabase/storage#1365 (comment))

Fix

When upload.size === 0 && !upload.sizeIsDeferred, complete the empty multipart in create, then completeMetadata and clear cache — same sequence as the end of write().

Update the existing zero-byte integration test to match server behavior (create only; no write()).

Test plan

  • Adjusted should successfully upload a zero byte file to assert object exists after create alone
  • CI for @tus/s3-store

Devin Review

When Upload-Length is 0, @tus/server treats the upload as final and
calls onUploadFinish without write(). Finish the empty multipart in
create so the object exists before finish hooks run.
@changeset-bot

changeset-bot Bot commented Sep 4, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 3d511f7

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

This PR includes changesets to release 1 package
Name Type
@tus/s3-store 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 1 potential issue.

Devin Review

Comment on lines +613 to +615
if (upload.size === 0 && !upload.sizeIsDeferred) {
const metadata = await this.getMetadata(upload.id)
await this.finishMultipartUpload(metadata, [])

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Empty creation requests now fail

With zero length and creation-with-upload, create completes the upload before the server invokes write. The valid request then fails because its upload is already closed.

Prompt for agents
Preserve valid creation-with-upload requests whose Upload-Length is zero. PostHandler always calls DataStore.write when the POST carries application/offset+octet-stream, but S3Store.create now completes the multipart upload first. The subsequent S3Store.write calls retrieveParts and receives NoSuchUpload. Coordinate the server and store behavior, or make the S3 store's zero-byte completion idempotent, so both create-only empty uploads and empty creation-with-upload requests succeed. Add coverage for a zero-length POST carrying the creation-with-upload content type.
Devin Review

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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Good catch — creation-with-upload does call write() after create even when Upload-Length is 0.

Addressed by making write idempotent: if retrieveParts returns NoSuchUpload and offset === size, drain the body and return the final offset. Added a regression test for zero-byte creation-with-upload.

creation-with-upload still calls write() after create completes an empty
multipart; treat NoSuchUpload as a no-op when offset already equals size.
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