diff --git a/.changeset/fix-blob-handle-upload-pathname.md b/.changeset/fix-blob-handle-upload-pathname.md new file mode 100644 index 000000000..1bfd520a4 --- /dev/null +++ b/.changeset/fix-blob-handle-upload-pathname.md @@ -0,0 +1,10 @@ +--- +"@vercel/blob": patch +--- + +fix(blob): respect pathname returned by onBeforeGenerateToken in handleUpload + +When using `handleUpload()`, the pathname returned from +`onBeforeGenerateToken()` is now used when generating the client token. +Previously, the SDK always used the original client-supplied pathname even when +the server returned an overridden pathname. diff --git a/packages/blob/src/client.node.test.ts b/packages/blob/src/client.node.test.ts index a1d8c9e95..c5a8af1b3 100644 --- a/packages/blob/src/client.node.test.ts +++ b/packages/blob/src/client.node.test.ts @@ -276,6 +276,45 @@ describe('client uploads', () => { }); }); + it('uses pathname returned by onBeforeGenerateToken in the generated client token', async () => { + const token = + 'vercel_blob_rw_12345fakeStoreId_30FakeRandomCharacters12345678'; + + const jsonResponse = await handleUpload({ + token, + request: { + headers: { 'x-vercel-signature': '123' }, + } as unknown as IncomingMessage, + body: { + type: 'blob.generate-client-token', + payload: { + pathname: 'client-supplied.txt', + clientPayload: null, + multipart: false, + }, + }, + onBeforeGenerateToken: async () => { + return Promise.resolve({ + pathname: 'server-controlled/final-name.txt', + callbackUrl: 'https://example.com', + }); + }, + onUploadCompleted: async () => { + await Promise.resolve(); + }, + }); + + expect(jsonResponse.type).toEqual('blob.generate-client-token'); + expect( + getPayloadFromClientToken((jsonResponse as any).clientToken), + ).toMatchObject({ + pathname: 'server-controlled/final-name.txt', + onUploadCompleted: { + callbackUrl: 'https://example.com', + }, + }); + }); + it('ignores client callbackUrl when server provides one', async () => { const token = 'vercel_blob_rw_12345fakeStoreId_30FakeRandomCharacters12345678'; diff --git a/packages/blob/src/client.ts b/packages/blob/src/client.ts index 8c7eb12f1..5a8738887 100644 --- a/packages/blob/src/client.ts +++ b/packages/blob/src/client.ts @@ -518,7 +518,7 @@ export interface HandleUploadOptions { * @param clientPayload - A string payload specified on the client when calling upload() * @param multipart - A boolean specifying whether the file is a multipart upload * - * @returns An object with configuration options for the client token including the optional callbackUrl + * @returns An object with configuration options for the client token including optional callbackUrl and pathname overrides */ onBeforeGenerateToken: ( pathname: string, @@ -534,6 +534,7 @@ export interface HandleUploadOptions { | 'allowOverwrite' | 'cacheControlMaxAge' | 'ifMatch' + | 'pathname' > & { tokenPayload?: string | null; callbackUrl?: string } >; @@ -592,7 +593,11 @@ export async function handleUpload({ multipart, ); const tokenPayload = payload.tokenPayload ?? clientPayload; - const { callbackUrl: providedCallbackUrl, ...tokenOptions } = payload; + const { + callbackUrl: providedCallbackUrl, + pathname: overriddenPathname, + ...tokenOptions + } = payload; let callbackUrl = providedCallbackUrl; // If onUploadCompleted is provided but no callbackUrl was provided, try to infer it from environment @@ -619,7 +624,7 @@ export async function handleUpload({ clientToken: await generateClientTokenFromReadWriteToken({ ...tokenOptions, token: resolvedToken, - pathname, + pathname: overriddenPathname ?? pathname, onUploadCompleted: callbackUrl ? { callbackUrl,