From 04ae0ec4e6e007bebca9cb5181cdbc6d90a4b226 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Tue, 16 Jun 2026 20:15:17 -0400 Subject: [PATCH] fix(upload): fail fast on 0-byte files in multipartUpload A 0-byte file produced a multipart upload with zero parts, which S3/R2 reject with a cryptic CompleteMultipartUpload error. Guard file.size === 0 up front with a clear message, before any S3 request is issued. Cloudflare-upload #4. Test: tests/multipart-empty-file-guard.test.mjs. Co-Authored-By: Claude Opus 4.8 --- js/storage/s3-multipart.js | 5 +++ tests/multipart-empty-file-guard.test.mjs | 38 +++++++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 tests/multipart-empty-file-guard.test.mjs diff --git a/js/storage/s3-multipart.js b/js/storage/s3-multipart.js index b56656ce..9bacb440 100644 --- a/js/storage/s3-multipart.js +++ b/js/storage/s3-multipart.js @@ -177,6 +177,11 @@ export async function multipartUpload({ if (!file) { throw new Error("File is required"); } + // A 0-byte file produces a multipart upload with zero parts, which S3/R2 + // rejects with a cryptic CompleteMultipartUpload error. Fail fast and clearly. + if (typeof file.size === "number" && file.size === 0) { + throw new Error("File is empty (0 bytes). Choose a non-empty file to upload."); + } const { CreateMultipartUploadCommand, diff --git a/tests/multipart-empty-file-guard.test.mjs b/tests/multipart-empty-file-guard.test.mjs new file mode 100644 index 00000000..f7a4ab25 --- /dev/null +++ b/tests/multipart-empty-file-guard.test.mjs @@ -0,0 +1,38 @@ +// Scenario (SCN-upload-empty-file): +// Given a 0-byte file, +// When multipartUpload is called, +// Then it rejects FAST with a clear "empty" message and never issues any S3 +// request (the old behavior completed a multipart upload with zero parts and +// surfaced a cryptic CompleteMultipartUpload error — Cloudflare-upload #4). + +// Disable the network import of the AWS SDK before importing the module. +globalThis.__BITVID_DISABLE_NETWORK_IMPORTS__ = true; + +import test from "node:test"; +import assert from "node:assert/strict"; + +const { multipartUpload } = await import("../js/storage/s3-multipart.js"); + +test("rejects a 0-byte file before any S3 request", async () => { + let sendCalled = false; + const s3 = { + send: async () => { + sendCalled = true; + return {}; + }, + }; + + await assert.rejects( + () => + multipartUpload({ + s3, + bucket: "bucket", + key: "u/npub/ns/empty.mp4", + file: { size: 0, type: "video/mp4" }, + }), + /empty|0 bytes/i, + "empty file should produce a clear error", + ); + + assert.equal(sendCalled, false, "must not start a multipart upload for an empty file"); +});