Skip to content

Commit

Permalink
fix(blob): Throw specific error on file extension mismatch (#770)
Browse files Browse the repository at this point in the history
* fix(blob): Throw specific error on file extension mismatch

We will now throw a specific error (`BlobContentTypeNotAllowed`) when the user
is trying to upload a file where the extension doesn't match what you've
configured as `allowedContentTypes` (Client Uploads).

* changeset
  • Loading branch information
vvo authored Oct 3, 2024
1 parent da87e89 commit 37d84ef
Show file tree
Hide file tree
Showing 4 changed files with 36 additions and 4 deletions.
5 changes: 5 additions & 0 deletions .changeset/nice-tools-destroy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@vercel/blob': patch
---

Throw specific error (BlobContentTypeNotAllowed) when file type doesn't match
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
BlobStoreNotFoundError,
BlobStoreSuspendedError,
BlobUnknownError,
BlobContentTypeNotAllowed,
requestApi,
} from './api';
import { BlobError } from './helpers';
Expand Down Expand Up @@ -64,7 +65,7 @@ describe('api', () => {
headers: {
authorization: 'Bearer 123',
'x-api-blob-request-attempt': '0',
'x-api-blob-request-id': ':1715951788049:b3a681154d83b',
'x-api-blob-request-id': expect.any(String) as string,
'x-api-version': '7',
},
method: 'POST',
Expand Down Expand Up @@ -103,18 +104,25 @@ describe('api', () => {
it.each([
[300, 'store_suspended', BlobStoreSuspendedError],
[400, 'forbidden', BlobAccessError],
[
400,
'forbidden',
BlobContentTypeNotAllowed,
'"contentType" text/plain is not allowed',
],
[500, 'not_found', BlobNotFoundError],
[600, 'bad_request', BlobError],
[700, 'store_not_found', BlobStoreNotFoundError],
[800, 'not_allowed', BlobUnknownError],
[800, 'not_allowed', BlobUnknownError],
])(
`should not retry '%s %s' response error response`,
async (status, code, error) => {
async (status, code, error, message = '') => {
const fetchMock = jest.spyOn(undici, 'fetch').mockImplementation(
jest.fn().mockResolvedValue({
status,
ok: false,
json: () => Promise.resolve({ error: { code } }),
json: () => Promise.resolve({ error: { code, message } }),
}),
);

Expand Down
20 changes: 19 additions & 1 deletion packages/blob/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ export class BlobAccessError extends BlobError {
}
}

export class BlobContentTypeNotAllowed extends BlobError {
constructor(message: string) {
super(`Content type mismatch, ${message}`);
}
}

export class BlobStoreNotFoundError extends BlobError {
constructor() {
super('This store does not exist.');
Expand Down Expand Up @@ -76,7 +82,8 @@ type BlobApiErrorCodes =
| 'store_not_found'
| 'not_allowed'
| 'service_unavailable'
| 'rate_limited';
| 'rate_limited'
| 'content_type_not_allowed';

export interface BlobApiError {
error?: { code?: BlobApiErrorCodes; message?: string };
Expand Down Expand Up @@ -152,6 +159,13 @@ async function getBlobError(
code = 'unknown_error';
}

// Now that we have multiple API clients out in the wild handling errors, we can't just send a different
// error code for this type of error. We need to add a new field in the API response to handle this correctly,
// but for now, we can just check the message.
if (message?.includes('contentType') && message.includes('is not allowed')) {
code = 'content_type_not_allowed';
}

let error: BlobError;
switch (code) {
case 'store_suspended':
Expand All @@ -160,6 +174,10 @@ async function getBlobError(
case 'forbidden':
error = new BlobAccessError();
break;
case 'content_type_not_allowed':
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- TS, be smarter
error = new BlobContentTypeNotAllowed(message!);
break;
case 'not_found':
error = new BlobNotFoundError();
break;
Expand Down
1 change: 1 addition & 0 deletions packages/blob/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export {
BlobServiceNotAvailable,
BlobRequestAbortedError,
BlobServiceRateLimited,
BlobContentTypeNotAllowed as BlobContentTypeNotAllowedError,
} from './api';

// vercelBlob.put()
Expand Down

0 comments on commit 37d84ef

Please sign in to comment.