vercel-blob
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseVercel Blob
Vercel Blob
Last Updated: 2026-01-21
Version: @vercel/blob@2.0.0
Skill Version: 2.1.0
最后更新时间: 2026-01-21
版本: @vercel/blob@2.0.0
技能版本: 2.1.0
Quick Start
快速开始
bash
undefinedbash
undefinedCreate Blob store: Vercel Dashboard → Storage → Blob
创建Blob存储:Vercel控制台 → 存储 → Blob
vercel env pull .env.local # Creates BLOB_READ_WRITE_TOKEN
npm install @vercel/blob
**Server Upload**:
```typescript
'use server';
import { put } from '@vercel/blob';
export async function uploadFile(formData: FormData) {
const file = formData.get('file') as File;
const blob = await put(file.name, file, { access: 'public' });
return blob.url;
}CRITICAL: Never expose to client. Use for client uploads.
BLOB_READ_WRITE_TOKENhandleUpload()vercel env pull .env.local # 生成BLOB_READ_WRITE_TOKEN
npm install @vercel/blob
**服务器端上传**:
```typescript
'use server';
import { put } from '@vercel/blob';
export async function uploadFile(formData: FormData) {
const file = formData.get('file') as File;
const blob = await put(file.name, file, { access: 'public' });
return blob.url;
}重要提示:切勿向客户端暴露。客户端上传请使用。
BLOB_READ_WRITE_TOKENhandleUpload()Client Upload (Secure)
安全客户端上传
Server Action (generates presigned token):
typescript
'use server';
import { handleUpload } from '@vercel/blob/client';
export async function getUploadToken(filename: string) {
return await handleUpload({
body: {
type: 'blob.generate-client-token',
payload: { pathname: `uploads/${filename}`, access: 'public' }
},
request: new Request('https://dummy'),
onBeforeGenerateToken: async (pathname) => ({
allowedContentTypes: ['image/jpeg', 'image/png'],
maximumSizeInBytes: 5 * 1024 * 1024
})
});
}Client Component:
typescript
'use client';
import { upload } from '@vercel/blob/client';
const tokenResponse = await getUploadToken(file.name);
const blob = await upload(file.name, file, {
access: 'public',
handleUploadUrl: tokenResponse.url
});服务器操作(生成预签名令牌):
typescript
'use server';
import { handleUpload } from '@vercel/blob/client';
export async function getUploadToken(filename: string) {
return await handleUpload({
body: {
type: 'blob.generate-client-token',
payload: { pathname: `uploads/${filename}`, access: 'public' }
},
request: new Request('https://dummy'),
onBeforeGenerateToken: async (pathname) => ({
allowedContentTypes: ['image/jpeg', 'image/png'],
maximumSizeInBytes: 5 * 1024 * 1024
})
});
}客户端组件:
typescript
'use client';
import { upload } from '@vercel/blob/client';
const tokenResponse = await getUploadToken(file.name);
const blob = await upload(file.name, file, {
access: 'public',
handleUploadUrl: tokenResponse.url
});File Management
文件管理
List/Delete:
typescript
import { list, del } from '@vercel/blob';
// List with pagination
const { blobs, cursor } = await list({ prefix: 'uploads/', cursor });
// Delete
await del(blobUrl);Multipart (>500MB):
typescript
import { createMultipartUpload, uploadPart, completeMultipartUpload } from '@vercel/blob';
const upload = await createMultipartUpload('large-video.mp4', { access: 'public' });
// Upload chunks in loop...
await completeMultipartUpload({ uploadId: upload.uploadId, parts });列出/删除:
typescript
import { list, del } from '@vercel/blob';
// 带分页的列表查询
const { blobs, cursor } = await list({ prefix: 'uploads/', cursor });
// 删除文件
await del(blobUrl);分块上传(>500MB):
typescript
import { createMultipartUpload, uploadPart, completeMultipartUpload } from '@vercel/blob';
const upload = await createMultipartUpload('large-video.mp4', { access: 'public' });
// 循环上传分片...
await completeMultipartUpload({ uploadId: upload.uploadId, parts });Critical Rules
关键规则
Always:
- ✅ Use for client uploads (never expose
handleUpload())BLOB_READ_WRITE_TOKEN - ✅ Validate file type/size before upload
- ✅ Use pathname organization (,
avatars/)uploads/ - ✅ Add timestamp/UUID to filenames (avoid collisions)
Never:
- ❌ Expose to client
BLOB_READ_WRITE_TOKEN - ❌ Upload >500MB without multipart
- ❌ Skip file validation
必须遵守:
- ✅ 客户端上传使用(切勿暴露
handleUpload())BLOB_READ_WRITE_TOKEN - ✅ 上传前验证文件类型/大小
- ✅ 使用路径名组织文件(如、
avatars/)uploads/ - ✅ 为文件名添加时间戳/UUID(避免冲突)
严禁操作:
- ❌ 向客户端暴露
BLOB_READ_WRITE_TOKEN - ❌ 上传超过500MB的文件时不使用分块上传
- ❌ 跳过文件验证
Known Issues Prevention
已知问题预防
This skill prevents 16 documented issues:
本技能可预防16种已记录的问题:
Issue #1: Missing Environment Variable
问题1:环境变量缺失
Error:
Source: https://vercel.com/docs/storage/vercel-blob
Why It Happens: Token not set in environment
Prevention: Run and ensure in .
Error: BLOB_READ_WRITE_TOKEN is not definedvercel env pull .env.local.env.local.gitignore错误信息:
来源: https://vercel.com/docs/storage/vercel-blob
原因: 未在环境中设置令牌
预防措施: 执行并确保已添加到中。
Error: BLOB_READ_WRITE_TOKEN is not definedvercel env pull .env.local.env.local.gitignoreIssue #2: Client Upload Token Exposed
问题2:客户端上传令牌暴露
Error: Security vulnerability, unauthorized uploads
Source: https://vercel.com/docs/storage/vercel-blob/client-upload
Why It Happens: Using directly in client code
Prevention: Use to generate client-specific tokens with constraints.
BLOB_READ_WRITE_TOKENhandleUpload()错误信息: 安全漏洞,未授权上传
来源: https://vercel.com/docs/storage/vercel-blob/client-upload
原因: 在客户端代码中直接使用
预防措施: 使用生成带约束的客户端专属令牌。
BLOB_READ_WRITE_TOKENhandleUpload()Issue #3: File Size Limit Exceeded
问题3:文件大小超限
Error: (500MB)
Source: https://vercel.com/docs/storage/vercel-blob/limits
Why It Happens: Uploading file >500MB without multipart upload
Prevention: Validate file size before upload, use multipart upload for large files.
Error: File size exceeds limit错误信息: (500MB)
来源: https://vercel.com/docs/storage/vercel-blob/limits
原因: 上传超过500MB的文件时未使用分块上传
预防措施: 上传前验证文件大小,大文件使用分块上传。
Error: File size exceeds limitIssue #4: Wrong Content-Type
问题4:Content-Type错误
Error: Browser downloads file instead of displaying (e.g., PDF opens as text)
Source: Production debugging
Why It Happens: Not setting option, Blob guesses incorrectly
Prevention: Always set or explicit MIME type.
contentTypecontentType: file.type错误信息: 浏览器下载文件而非直接显示(如PDF以文本形式打开)
来源: 生产环境调试
原因: 未设置选项,Blob自动猜测错误
预防措施: 始终设置或明确的MIME类型。
contentTypecontentType: file.typeIssue #5: Public File Not Cached
问题5:公开文件未被缓存
Error: Slow file delivery, high egress costs
Source: Vercel Blob best practices
Why It Happens: Using for files that should be public
Prevention: Use for publicly accessible files (CDN caching).
access: 'private'access: 'public'错误信息: 文件分发缓慢,出口流量成本高
来源: Vercel Blob最佳实践
原因: 对应公开的文件使用了
预防措施: 对公开可访问的文件使用(支持CDN缓存)。
access: 'private'access: 'public'Issue #6: List Pagination Not Handled
问题6:未处理列表分页
Error: Only first 1000 files returned, missing files
Source: https://vercel.com/docs/storage/vercel-blob/using-blob-sdk#list
Why It Happens: Not iterating with cursor for large file lists
Prevention: Use cursor-based pagination in loop until is undefined.
cursor错误信息: 仅返回前1000个文件,存在文件缺失
来源: https://vercel.com/docs/storage/vercel-blob/using-blob-sdk#list
原因: 未对大文件列表使用游标进行迭代
预防措施: 使用基于游标的分页循环查询,直到为undefined。
cursorIssue #7: Delete Fails Silently
问题7:删除操作静默失败
Error: Files not deleted, storage quota fills up
Source: https://github.com/vercel/storage/issues/150
Why It Happens: Using wrong URL format, blob not found
Prevention: Use full blob URL from response, check deletion result.
put()错误信息: 文件未被删除,存储配额被占满
来源: https://github.com/vercel/storage/issues/150
原因: 使用错误的URL格式,未找到Blob文件
预防措施: 使用返回的完整Blob URL,并检查删除结果。
put()Issue #8: Upload Timeout (Large Files) + Server-Side 4.5MB Limit
问题8:上传超时(大文件)+ 服务器端4.5MB限制
Error: for files >100MB (server) OR file upload fails at 4.5MB (serverless function limit)
Source: Vercel function timeout limits + 4.5MB serverless limit + Community Discussion
Why It Happens:
Error: Request timeout- Serverless function timeout (10s free tier, 60s pro) for server-side uploads
- CRITICAL: Vercel serverless functions have a hard 4.5MB request body limit. Using in server actions/API routes fails for files >4.5MB.
put()
Prevention: Use client-side upload with for files >4.5MB OR use multipart upload.
handleUpload()typescript
// ❌ Server-side upload fails at 4.5MB
export async function POST(request: Request) {
const formData = await request.formData();
const file = formData.get('file') as File; // Fails if >4.5MB
await put(file.name, file, { access: 'public' });
}
// ✅ Client upload bypasses 4.5MB limit (supports up to 500MB)
const blob = await upload(file.name, file, {
access: 'public',
handleUploadUrl: '/api/upload/token',
multipart: true, // For files >500MB, use multipart
});错误信息: 服务器端上传大文件(>100MB)时出现,或文件上传在4.5MB处失败(无服务器函数限制)
来源: Vercel函数超时限制 + 4.5MB无服务器限制 + 社区讨论
原因:
Error: Request timeout- 服务器端上传时无服务器函数超时(免费层10秒,专业版60秒)
- 重要提示: Vercel无服务器函数存在4.5MB的请求体硬限制。在服务器操作/API路由中使用上传超过4.5MB的文件会失败。
put()
预防措施: 对于超过4.5MB的文件,使用进行客户端上传,或使用分块上传。
handleUpload()typescript
// ❌ 服务器端上传在4.5MB处失败
export async function POST(request: Request) {
const formData = await request.formData();
const file = formData.get('file') as File; // 文件超过4.5MB时失败
await put(file.name, file, { access: 'public' });
}
// ✅ 客户端上传绕过4.5MB限制(支持最大500MB)
const blob = await upload(file.name, file, {
access: 'public',
handleUploadUrl: '/api/upload/token',
multipart: true, // 超过500MB时使用分块上传
});Issue #9: Filename Collisions
问题9:文件名冲突
Error: Files overwritten, data loss
Source: Production debugging
Why It Happens: Using same filename for multiple uploads
Prevention: Add timestamp/UUID: or .
`uploads/${Date.now()}-${file.name}`addRandomSuffix: true错误信息: 文件被覆盖,数据丢失
来源: 生产环境调试
原因: 多个上传使用相同文件名
预防措施: 添加时间戳/UUID: 或使用。
`uploads/${Date.now()}-${file.name}`addRandomSuffix: trueIssue #10: Missing Upload Callback
问题10:缺少上传回调
Error: Upload completes but app state not updated
Source: https://vercel.com/docs/storage/vercel-blob/client-upload#callback-after-upload
Why It Happens: Not implementing callback
Prevention: Use in to update database/state.
onUploadCompletedonUploadCompletedhandleUpload()错误信息: 上传完成但应用状态未更新
来源: https://vercel.com/docs/storage/vercel-blob/client-upload#callback-after-upload
原因: 未实现回调
预防措施: 在中使用来更新数据库/状态。
onUploadCompletedhandleUpload()onUploadCompletedIssue #11: Client Upload Token Expiration for Large Files
问题11:大文件上传时客户端令牌过期
Error:
Source: GitHub Issue #443
Why It Happens: Default token expires after 30 seconds. Large files (>100MB) take longer to upload, causing token expiration before validation.
Prevention: Set parameter for large file uploads.
Error: Access denied, please provide a valid token for this resourcevalidUntiltypescript
// For large files (>100MB), extend token expiration
const jsonResponse = await handleUpload({
body,
request,
onBeforeGenerateToken: async (pathname) => {
return {
maximumSizeInBytes: 200 * 1024 * 1024,
validUntil: Date.now() + 300000, // 5 minutes
};
},
});错误信息:
来源: GitHub Issue #443
原因: 默认令牌30秒后过期。大文件(>100MB)上传耗时超过30秒,导致验证前令牌过期。
预防措施: 为大文件上传设置参数。
Error: Access denied, please provide a valid token for this resourcevalidUntiltypescript
// 大文件(>100MB)上传时延长令牌有效期
const jsonResponse = await handleUpload({
body,
request,
onBeforeGenerateToken: async (pathname) => {
return {
maximumSizeInBytes: 200 * 1024 * 1024,
validUntil: Date.now() + 300000, // 5分钟
};
},
});Issue #12: v2.0.0 Breaking Change - onUploadCompleted Requires callbackUrl (Non-Vercel Hosting)
问题12:v2.0.0破坏性变更 - 非Vercel托管时onUploadCompleted需要callbackUrl
Error: onUploadCompleted callback doesn't fire when not hosted on Vercel
Source: Release Notes @vercel/blob@2.0.0
Why It Happens: v2.0.0 removed automatic callback URL inference from client-side for security. When not using Vercel system environment variables, you must explicitly provide .
Prevention: Explicitly provide in for non-Vercel hosting.
location.hrefcallbackUrlcallbackUrlonBeforeGenerateTokentypescript
// v2.0.0+ for non-Vercel hosting
await handleUpload({
body,
request,
onBeforeGenerateToken: async (pathname) => {
return {
callbackUrl: 'https://example.com', // Required for non-Vercel hosting
};
},
onUploadCompleted: async ({ blob, tokenPayload }) => {
// Now fires correctly
},
});
// For local development with ngrok:
// VERCEL_BLOB_CALLBACK_URL=https://abc123.ngrok-free.app错误信息: 未托管在Vercel上时onUploadCompleted回调不触发
来源: Release Notes @vercel/blob@2.0.0
原因: v2.0.0为了安全,移除了从客户端自动推断回调URL的功能。当不使用Vercel系统环境变量时,必须显式提供。
预防措施: 对于非Vercel托管环境,在中显式提供。
location.hrefcallbackUrlonBeforeGenerateTokencallbackUrltypescript
// v2.0.0+ 非Vercel托管环境使用
await handleUpload({
body,
request,
onBeforeGenerateToken: async (pathname) => {
return {
callbackUrl: 'https://example.com', // 非Vercel托管时必填
};
},
onUploadCompleted: async ({ blob, tokenPayload }) => {
// 现在可正常触发
},
});
// ngrok本地开发时:
// VERCEL_BLOB_CALLBACK_URL=https://abc123.ngrok-free.appIssue #13: ReadableStream Upload Not Supported in Firefox
问题13:Firefox不支持ReadableStream上传
Error: Upload never completes in Firefox
Source: GitHub Issue #881
Why It Happens: The TypeScript interface accepts as a body type, but Firefox does not support as a fetch body.
Prevention: Convert stream to Blob or ArrayBuffer for cross-browser support.
ReadableStreamReadableStreamtypescript
// ❌ Works in Chrome/Edge, hangs in Firefox
const stream = new ReadableStream({ /* ... */ });
await put('file.bin', stream, { access: 'public' }); // Never completes in Firefox
// ✅ Convert stream to Blob for cross-browser support
const chunks: Uint8Array[] = [];
const reader = stream.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
}
const blob = new Blob(chunks);
await put('file.bin', blob, { access: 'public' });错误信息: Firefox中上传始终无法完成
来源: GitHub Issue #881
原因: TypeScript接口接受作为主体类型,但Firefox不支持将作为fetch主体。
预防措施: 将流转换为Blob或ArrayBuffer以实现跨浏览器支持。
ReadableStreamReadableStreamtypescript
// ❌ 在Chrome/Edge中可用,在Firefox中挂起
const stream = new ReadableStream({ /* ... */ });
await put('file.bin', stream, { access: 'public' }); // 在Firefox中始终无法完成
// ✅ 将流转换为Blob以实现跨浏览器支持
const chunks: Uint8Array[] = [];
const reader = stream.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
}
const blob = new Blob(chunks);
await put('file.bin', blob, { access: 'public' });Issue #14: Pathname Cannot Be Modified in onBeforeGenerateToken
问题14:onBeforeGenerateToken中无法修改Pathname
Error: File uploaded to wrong path despite server-side pathname override attempt
Source: GitHub Issue #863
Why It Happens: The parameter in cannot be changed. It's set at time on the client side.
Prevention: Construct pathname on client, validate on server. Use to pass metadata.
pathnameonBeforeGenerateTokenupload(pathname, ...)clientPayloadtypescript
// Client: Construct pathname before upload
await upload(`uploads/${Date.now()}-${file.name}`, file, {
access: 'public',
handleUploadUrl: '/api/upload',
clientPayload: JSON.stringify({ userId: '123' }),
});
// Server: Validate pathname matches expected pattern
await handleUpload({
body,
request,
onBeforeGenerateToken: async (pathname, clientPayload) => {
const { userId } = JSON.parse(clientPayload || '{}');
// Validate pathname starts with expected prefix
if (!pathname.startsWith(`uploads/`)) {
throw new Error('Invalid upload path');
}
return {
allowedContentTypes: ['image/jpeg', 'image/png'],
tokenPayload: JSON.stringify({ userId }), // Pass to onUploadCompleted
};
},
});错误信息: 尽管尝试在服务器端覆盖路径名,文件仍上传到错误路径
来源: GitHub Issue #863
原因: 中的参数无法修改,它是在客户端时设置的。
预防措施: 在客户端构造路径名,在服务器端验证。使用传递元数据。
onBeforeGenerateTokenpathnameupload(pathname, ...)clientPayloadtypescript
// 客户端:上传前构造路径名
await upload(`uploads/${Date.now()}-${file.name}`, file, {
access: 'public',
handleUploadUrl: '/api/upload',
clientPayload: JSON.stringify({ userId: '123' }),
});
// 服务器端:验证路径名是否符合预期格式
await handleUpload({
body,
request,
onBeforeGenerateToken: async (pathname, clientPayload) => {
const { userId } = JSON.parse(clientPayload || '{}');
// 验证路径名是否以预期前缀开头
if (!pathname.startsWith(`uploads/`)) {
throw new Error('无效上传路径');
}
return {
allowedContentTypes: ['image/jpeg', 'image/png'],
tokenPayload: JSON.stringify({ userId }), // 传递给onUploadCompleted
};
},
});Issue #15: Multipart Upload Minimum Chunk Size (5MB)
问题15:分块上传最小分片大小(5MB)
Error: Manual multipart upload fails with small chunks
Source: Official Docs + Community Discussion
Why It Happens: Each part in manual multipart upload must be at least 5MB (except the last part). This conflicts with Vercel's 4.5MB serverless function limit, making manual multipart uploads impossible via server-side routes.
Prevention: Use automatic multipart ( in ) or client uploads.
multipart: trueput()typescript
// ❌ Manual multipart upload fails (can't upload 5MB chunks via serverless function)
const upload = await createMultipartUpload('large.mp4', { access: 'public' });
// uploadPart() requires 5MB minimum - hits serverless limit
// ✅ Use automatic multipart via client upload
await upload('large.mp4', file, {
access: 'public',
handleUploadUrl: '/api/upload',
multipart: true, // Automatically handles 5MB+ chunks
});错误信息: 手动分块上传在使用小分片时失败
来源: 官方文档 + 社区讨论
原因: 手动分块上传的每个分片至少为5MB(最后一个分片除外)。这与Vercel的4.5MB无服务器函数限制冲突,导致无法通过服务器端路由进行手动分块上传。
预防措施: 使用自动分块上传(中设置)或客户端上传。
put()multipart: truetypescript
// ❌ 手动分块上传失败(无法通过无服务器函数上传5MB分片)
const upload = await createMultipartUpload('large.mp4', { access: 'public' });
// uploadPart()要求最小5MB - 触发无服务器限制
// ✅ 通过客户端上传使用自动分块
await upload('large.mp4', file, {
access: 'public',
handleUploadUrl: '/api/upload',
multipart: true, // 自动处理5MB+分片
});Issue #16: Missing File Extension Causes Access Denied Error
问题16:缺少文件扩展名导致访问被拒绝错误
Error:
Source: GitHub Issue #664
Why It Happens: Pathname without file extension causes non-descriptive access denied error.
Prevention: Always include file extension in pathname.
Error: Access denied, please provide a valid token for this resourcetypescript
// ❌ Fails with confusing error
await upload('user-12345', file, {
access: 'public',
handleUploadUrl: '/api/upload',
}); // Error: Access denied
// ✅ Extract extension and include in pathname
const extension = file.name.split('.').pop();
await upload(`user-${userId}.${extension}`, file, {
access: 'public',
handleUploadUrl: '/api/upload',
});错误信息:
来源: GitHub Issue #664
原因: 路径名中缺少文件扩展名导致模糊的访问被拒绝错误。
预防措施: 路径名中始终包含文件扩展名。
Error: Access denied, please provide a valid token for this resourcetypescript
// ❌ 触发模糊错误
await upload('user-12345', file, {
access: 'public',
handleUploadUrl: '/api/upload',
}); // 错误:Access denied
// ✅ 提取扩展名并包含在路径名中
const extension = file.name.split('.').pop();
await upload(`user-${userId}.${extension}`, file, {
access: 'public',
handleUploadUrl: '/api/upload',
});Common Patterns
常见模式
Avatar Upload with Replacement:
typescript
'use server';
import { put, del } from '@vercel/blob';
export async function updateAvatar(userId: string, formData: FormData) {
const file = formData.get('avatar') as File;
if (!file.type.startsWith('image/')) throw new Error('Only images allowed');
const user = await db.query.users.findFirst({ where: eq(users.id, userId) });
if (user?.avatarUrl) await del(user.avatarUrl); // Delete old
const blob = await put(`avatars/${userId}.jpg`, file, { access: 'public' });
await db.update(users).set({ avatarUrl: blob.url }).where(eq(users.id, userId));
return blob.url;
}Protected Upload ():
access: 'private'typescript
const blob = await put(`documents/${userId}/${file.name}`, file, { access: 'private' });头像上传与替换:
typescript
'use server';
import { put, del } from '@vercel/blob';
export async function updateAvatar(userId: string, formData: FormData) {
const file = formData.get('avatar') as File;
if (!file.type.startsWith('image/')) throw new Error('仅允许上传图片');
const user = await db.query.users.findFirst({ where: eq(users.id, userId) });
if (user?.avatarUrl) await del(user.avatarUrl); // 删除旧头像
const blob = await put(`avatars/${userId}.jpg`, file, { access: 'public' });
await db.update(users).set({ avatarUrl: blob.url }).where(eq(users.id, userId));
return blob.url;
}受保护的上传():
access: 'private'typescript
const blob = await put(`documents/${userId}/${file.name}`, file, { access: 'private' });