From 37d84ef0a246bc24917a46c3c1e139c818f8ff7a Mon Sep 17 00:00:00 2001 From: Vincent Voyer Date: Thu, 3 Oct 2024 15:26:26 +0200 Subject: [PATCH] fix(blob): Throw specific error on file extension mismatch (#770) * 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 --- .changeset/nice-tools-destroy.md | 5 +++++ .../src/{api.test.ts => api.node.test.ts} | 14 ++++++++++--- packages/blob/src/api.ts | 20 ++++++++++++++++++- packages/blob/src/index.ts | 1 + 4 files changed, 36 insertions(+), 4 deletions(-) create mode 100644 .changeset/nice-tools-destroy.md rename packages/blob/src/{api.test.ts => api.node.test.ts} (89%) diff --git a/.changeset/nice-tools-destroy.md b/.changeset/nice-tools-destroy.md new file mode 100644 index 000000000..8e974bfb1 --- /dev/null +++ b/.changeset/nice-tools-destroy.md @@ -0,0 +1,5 @@ +--- +'@vercel/blob': patch +--- + +Throw specific error (BlobContentTypeNotAllowed) when file type doesn't match diff --git a/packages/blob/src/api.test.ts b/packages/blob/src/api.node.test.ts similarity index 89% rename from packages/blob/src/api.test.ts rename to packages/blob/src/api.node.test.ts index 4467c7b46..f23799ceb 100644 --- a/packages/blob/src/api.test.ts +++ b/packages/blob/src/api.node.test.ts @@ -6,6 +6,7 @@ import { BlobStoreNotFoundError, BlobStoreSuspendedError, BlobUnknownError, + BlobContentTypeNotAllowed, requestApi, } from './api'; import { BlobError } from './helpers'; @@ -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', @@ -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 } }), }), ); diff --git a/packages/blob/src/api.ts b/packages/blob/src/api.ts index 7d5f62c60..7780378ed 100644 --- a/packages/blob/src/api.ts +++ b/packages/blob/src/api.ts @@ -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.'); @@ -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 }; @@ -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': @@ -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; diff --git a/packages/blob/src/index.ts b/packages/blob/src/index.ts index 4519945d8..69674a202 100644 --- a/packages/blob/src/index.ts +++ b/packages/blob/src/index.ts @@ -21,6 +21,7 @@ export { BlobServiceNotAvailable, BlobRequestAbortedError, BlobServiceRateLimited, + BlobContentTypeNotAllowed as BlobContentTypeNotAllowedError, } from './api'; // vercelBlob.put()