Skip to content

Commit c9d9a1a

Browse files
vvoclaude
andauthored
[@vercel/blob] Apply ifMatch/allowOverwrite validation to handleUpload and generateClientToken (#1027)
* [@vercel/blob] Apply ifMatch/allowOverwrite validation to handleUpload and generateClientToken The same contradiction (ifMatch + allowOverwrite: false) can happen via handleUpload's onBeforeGenerateToken callback or direct generateClientTokenFromReadWriteToken calls. Add validation there too. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * [@vercel/blob] Fix biome formatting Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 6dcecb8 commit c9d9a1a

File tree

3 files changed

+51
-0
lines changed

3 files changed

+51
-0
lines changed
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+
Apply `ifMatch`/`allowOverwrite` validation to `handleUpload` and `generateClientTokenFromReadWriteToken`. When `ifMatch` is set via `onBeforeGenerateToken` or direct token generation, `allowOverwrite` is now implicitly enabled. Explicitly passing `allowOverwrite: false` with `ifMatch` throws a clear error.

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,34 @@ describe('client uploads', () => {
3939
});
4040
});
4141

42+
it('throws when ifMatch is used with allowOverwrite: false', async () => {
43+
await expect(
44+
generateClientTokenFromReadWriteToken({
45+
pathname: 'foo.txt',
46+
token:
47+
'vercel_blob_rw_12345fakeStoreId_30FakeRandomCharacters12345678',
48+
ifMatch: '"abc123"',
49+
allowOverwrite: false,
50+
}),
51+
).rejects.toThrow(
52+
'ifMatch and allowOverwrite: false are contradictory. ifMatch is used for conditional overwrites, which requires allowOverwrite to be true.',
53+
);
54+
});
55+
56+
it('implicitly sets allowOverwrite when ifMatch is provided', async () => {
57+
const uploadToken = await generateClientTokenFromReadWriteToken({
58+
pathname: 'foo.txt',
59+
token: 'vercel_blob_rw_12345fakeStoreId_30FakeRandomCharacters12345678',
60+
ifMatch: '"abc123"',
61+
});
62+
63+
expect(getPayloadFromClientToken(uploadToken)).toMatchObject({
64+
pathname: 'foo.txt',
65+
ifMatch: '"abc123"',
66+
allowOverwrite: true,
67+
});
68+
});
69+
4270
it('accepts a tokenPayload property', async () => {
4371
const uploadToken = await generateClientTokenFromReadWriteToken({
4472
pathname: 'foo.txt',

packages/blob/src/client.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -755,6 +755,24 @@ export async function generateClientTokenFromReadWriteToken({
755755
);
756756
}
757757

758+
// ifMatch implies allowOverwrite — updating a blob by ETag inherently requires
759+
// overwriting. Throw if the user explicitly contradicts this.
760+
if (argsWithoutToken.ifMatch && argsWithoutToken.allowOverwrite === false) {
761+
throw new BlobError(
762+
'ifMatch and allowOverwrite: false are contradictory. ifMatch is used for conditional overwrites, which requires allowOverwrite to be true.',
763+
);
764+
}
765+
766+
// Implicitly enable allowOverwrite when ifMatch is set and allowOverwrite
767+
// was not explicitly provided, to prevent the server from sending
768+
// conflicting If-Match + If-None-Match headers to S3.
769+
if (
770+
argsWithoutToken.ifMatch &&
771+
argsWithoutToken.allowOverwrite === undefined
772+
) {
773+
argsWithoutToken.allowOverwrite = true;
774+
}
775+
758776
const timestamp = new Date();
759777
timestamp.setSeconds(timestamp.getSeconds() + 30);
760778
const readWriteToken = getTokenFromOptionsOrEnv({ token });

0 commit comments

Comments
 (0)