Skip to content

Commit 2b4acc3

Browse files
authored
feat(@vercel/blob): Allow custom headers in client uploads (#857)
Fixes #796 #420
1 parent 1ade8df commit 2b4acc3

File tree

6 files changed

+55
-0
lines changed

6 files changed

+55
-0
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@vercel/blob": minor
3+
---
4+
5+
feat(blob): Add support for custom headers in client upload method
6+
7+
This change adds the ability to pass custom headers to the `upload` method in the client, which will be forwarded to the server endpoint specified by `handleUploadUrl`. This is particularly useful for sending authorization headers and solves issues like [#796](https://github.com/vercel/storage/issues/796) and [#420](https://github.com/vercel/storage/issues/420).

packages/blob/src/client.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,11 @@ export interface CommonUploadOptions {
234234
* Additional data which will be sent to your `handleUpload` route.
235235
*/
236236
clientPayload?: string;
237+
/**
238+
* Additional headers to be sent when making the request to your `handleUpload` route.
239+
* This is useful for sending authorization headers or any other custom headers.
240+
*/
241+
headers?: Record<string, string>;
237242
}
238243

239244
/**
@@ -255,6 +260,7 @@ export type UploadOptions = ClientCommonPutOptions & CommonUploadOptions;
255260
* - access - (Required) Must be 'public' as blobs are publicly accessible.
256261
* - handleUploadUrl - (Required) A string specifying the route to call for generating client tokens for client uploads.
257262
* - clientPayload - (Optional) A string to be sent to your handleUpload server code. Example use-case: attaching the post id an image relates to.
263+
* - headers - (Optional) An object containing custom headers to be sent with the request to your handleUpload route. Example use-case: sending Authorization headers.
258264
* - contentType - (Optional) A string indicating the media type. By default, it's extracted from the pathname's extension.
259265
* - multipart - (Optional) Whether to use multipart upload for large files. It will split the file into multiple parts, upload them in parallel and retry failed parts.
260266
* - abortSignal - (Optional) AbortSignal to cancel the operation.
@@ -290,6 +296,7 @@ export const upload = createPutMethod<UploadOptions>({
290296
pathname,
291297
clientPayload: options.clientPayload ?? null,
292298
multipart: options.multipart ?? false,
299+
headers: options.headers,
293300
});
294301
},
295302
});
@@ -643,6 +650,7 @@ async function retrieveClientToken(options: {
643650
clientPayload: string | null;
644651
multipart: boolean;
645652
abortSignal?: AbortSignal;
653+
headers?: Record<string, string>;
646654
}): Promise<string> {
647655
const { handleUploadUrl, pathname } = options;
648656
const url = isAbsoluteUrl(handleUploadUrl)
@@ -664,6 +672,7 @@ async function retrieveClientToken(options: {
664672
body: JSON.stringify(event),
665673
headers: {
666674
'content-type': 'application/json',
675+
...options.headers,
667676
},
668677
signal: options.abortSignal,
669678
});

test/next/src/app/vercel/blob/app/client/page.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ export default function AppClientUpload(): React.JSX.Element {
3333
access: 'public',
3434
multipart: searchParams?.get('multipart') === '1',
3535
handleUploadUrl: `/vercel/blob/api/app/handle-blob-upload/serverless`,
36+
// Example of using the new headers parameter to send an authorization header
37+
headers: {
38+
Authorization: 'Bearer your-token-here',
39+
'X-Custom-Header': 'Custom Value',
40+
},
3641
onUploadProgress(progressEvent) {
3742
setProgressEvents((prev) => [
3843
...prev,

test/next/src/app/vercel/blob/app/test/client/page.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ export default function AppBodyClient(props: {
2121
access: 'public',
2222
handleUploadUrl: callback,
2323
multipart: multipart === '1',
24+
// Example of using the new headers parameter to send an authorization header
25+
headers: {
26+
Authorization: 'Bearer test-token',
27+
'X-Test-Header': 'Test Value',
28+
},
2429
});
2530
setBlob(blobResult);
2631
setContent(await fetch(blobResult.url).then((r) => r.text()));

test/next/src/app/vercel/blob/handle-blob-upload.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@ export async function handleUploadHandler(
2626
request: Request,
2727
): Promise<NextResponse> {
2828
const body = (await request.json()) as HandleUploadBody;
29+
30+
// Log all headers received
31+
console.log('Request headers:');
32+
request.headers.forEach((value, key) => {
33+
console.log(`${key}: ${value}`);
34+
});
35+
2936
try {
3037
const jsonResponse = await handleUpload({
3138
body,
@@ -38,10 +45,17 @@ export async function handleUploadHandler(
3845
throw new Error('Not authorized');
3946
}
4047

48+
// You can now access headers in the authorization logic
49+
const customHeader =
50+
request.headers.get('X-Custom-Header') ||
51+
request.headers.get('X-Test-Header');
52+
console.log('Custom header received:', customHeader);
53+
4154
return {
4255
addRandomSuffix: true,
4356
tokenPayload: JSON.stringify({
4457
userId: user?.id,
58+
customHeader,
4559
}),
4660
};
4761
},

test/next/src/app/vercel/blob/validate-upload-token.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,21 @@ export function validateUploadToken(
44
request: IncomingMessage | Request,
55
): boolean {
66
if (process.env.NODE_ENV === 'development') return true;
7+
8+
// Check for authorization header
9+
const authHeader =
10+
'credentials' in request
11+
? (request.headers.get('authorization') ?? '')
12+
: (request.headers.authorization ?? '');
13+
14+
// Validate Bearer token if present
15+
if (authHeader && authHeader.startsWith('Bearer ')) {
16+
const token = authHeader.split(' ')[1];
17+
// Example validation - in real app, you'd validate against your auth system
18+
return token === 'your-token-here' || token === 'test-token';
19+
}
20+
21+
// Fall back to cookie validation
722
const cookie =
823
'credentials' in request
924
? (request.headers.get('cookie') ?? '')

0 commit comments

Comments
 (0)