Skip to content

Commit d2ea7cf

Browse files
matingathanivvoclaude
authored
fix(blob): enforce maximumSizeInBytes client-side for multipart uploads (#1036)
For bodies with a known size (Blob, File, Buffer), check maximumSizeInBytes before starting the multipart upload. This avoids creating a multipart upload and uploading parts that will ultimately be rejected by the server for exceeding the size limit. Streams are skipped since their size is unknown upfront. Fixes #873 Co-authored-by: Vincent Voyer <vincent@codeagain.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 7f34f12 commit d2ea7cf

4 files changed

Lines changed: 68 additions & 3 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@vercel/blob": patch
3+
---
4+
5+
Enforce `maximumSizeInBytes` client-side for multipart uploads. Bodies with a known size (Blob, File, Buffer) are now checked before the upload starts, avoiding wasted API calls.

packages/blob/src/helpers.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,14 @@ export interface CommonCreateBlobOptions extends BlobCommandOptions {
6464
* If the ETag doesn't match, a `BlobPreconditionFailedError` will be thrown.
6565
*/
6666
ifMatch?: string;
67+
/**
68+
* Maximum size in bytes allowed for this upload. Currently only enforced
69+
* client-side for multipart uploads (`put(..., { multipart: true })`).
70+
* For bodies with a known size (Blob, File, Buffer, etc.) the check is
71+
* performed before the upload starts. Streams cannot be checked upfront.
72+
* The maximum allowed value is 5TB.
73+
*/
74+
maximumSizeInBytes?: number;
6775
}
6876

6977
/**

packages/blob/src/index.node.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -753,6 +753,44 @@ describe('blob client', () => {
753753
);
754754
});
755755

756+
it('throws when body exceeds maximumSizeInBytes in a multipart put', async () => {
757+
const largeBody = new Blob([new Uint8Array(1024)]); // 1 KB
758+
await expect(
759+
put('foo.txt', largeBody, {
760+
access: 'public',
761+
multipart: true,
762+
maximumSizeInBytes: 512, // 512 bytes limit
763+
}),
764+
).rejects.toThrow(
765+
new Error(
766+
'Vercel Blob: Body size of 1024 bytes exceeds the maximum allowed size of 512 bytes',
767+
),
768+
);
769+
});
770+
771+
it('does not throw client-side size error for stream body with maximumSizeInBytes in a multipart put', async () => {
772+
const stream = new ReadableStream({
773+
start(controller) {
774+
controller.enqueue(new TextEncoder().encode('hello'));
775+
controller.close();
776+
},
777+
});
778+
779+
// Should not throw the client-side size error — streams have unknown
780+
// length, so maximumSizeInBytes enforcement is deferred to the server.
781+
try {
782+
await put('foo.txt', stream, {
783+
access: 'public',
784+
multipart: true,
785+
maximumSizeInBytes: 1,
786+
});
787+
} catch (error) {
788+
expect((error as Error).message).not.toMatch(
789+
/Body size of .* bytes exceeds the maximum allowed size/,
790+
);
791+
}
792+
});
793+
756794
const table: [string, (signal: AbortSignal) => Promise<unknown>][] = [
757795
[
758796
'put',

packages/blob/src/multipart/uncontrolled.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { debug } from '../debug';
2-
import type { BlobCommandOptions, WithUploadProgress } from '../helpers';
3-
import { computeBodyLength } from '../helpers';
2+
import type { CommonCreateBlobOptions, WithUploadProgress } from '../helpers';
3+
import { BlobError, computeBodyLength, isStream } from '../helpers';
44
import type { PutBlobResult, PutBody } from '../put-helpers';
55
import { completeMultipartUpload } from './complete';
66
import { createMultipartUpload } from './create';
@@ -12,7 +12,7 @@ export async function uncontrolledMultipartUpload(
1212
pathname: string,
1313
body: PutBody,
1414
headers: Record<string, string>,
15-
options: BlobCommandOptions & WithUploadProgress,
15+
options: CommonCreateBlobOptions & WithUploadProgress,
1616
): Promise<PutBlobResult> {
1717
debug('mpu: init', 'pathname:', pathname, 'headers:', headers);
1818

@@ -21,6 +21,20 @@ export async function uncontrolledMultipartUpload(
2121
onUploadProgress: undefined,
2222
};
2323

24+
// For bodies with a known size (Blob, File, Buffer, etc.) enforce
25+
// maximumSizeInBytes client-side before starting the upload. This avoids
26+
// creating a multipart upload that will ultimately fail.
27+
// Streams are skipped because their size is unknown upfront.
28+
if (
29+
options.maximumSizeInBytes !== undefined &&
30+
!isStream(body) &&
31+
computeBodyLength(body) > options.maximumSizeInBytes
32+
) {
33+
throw new BlobError(
34+
`Body size of ${computeBodyLength(body)} bytes exceeds the maximum allowed size of ${options.maximumSizeInBytes} bytes`,
35+
);
36+
}
37+
2438
// Step 1: Start multipart upload
2539
const createMultipartUploadResponse = await createMultipartUpload(
2640
pathname,

0 commit comments

Comments
 (0)