npx skills add https://github.com/jezweb/claude-skills --skill vercel-blob最后更新 : 2026-01-21 版本 : @vercel/blob@2.0.0 技能版本 : 2.1.0
# 创建 Blob 存储:Vercel 控制台 → 存储 → Blob
vercel env pull .env.local # 创建 BLOB_READ_WRITE_TOKEN
npm install @vercel/blob
服务器端上传 :
'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_TOKEN 暴露给客户端。客户端上传请使用 handleUpload()。
服务器操作(生成预签名令牌):
'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
})
});
}
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
客户端组件 :
'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
});
列出/删除 :
import { list, del } from '@vercel/blob';
// 分页列出
const { blobs, cursor } = await list({ prefix: 'uploads/', cursor });
// 删除
await del(blobUrl);
多部分上传(>500MB):
import { createMultipartUpload, uploadPart, completeMultipartUpload } from '@vercel/blob';
const upload = await createMultipartUpload('large-video.mp4', { access: 'public' });
// 在循环中上传分片...
await completeMultipartUpload({ uploadId: upload.uploadId, parts });
始终 :
handleUpload()(切勿暴露 BLOB_READ_WRITE_TOKEN)avatars/、uploads/)切勿 :
BLOB_READ_WRITE_TOKEN 暴露给客户端此技能可预防 16 个已记录的问题 :
错误 : Error: BLOB_READ_WRITE_TOKEN is not defined 来源 : https://vercel.com/docs/storage/vercel-blob 发生原因 : 环境中未设置令牌 预防措施 : 运行 vercel env pull .env.local 并确保 .env.local 在 .gitignore 中。
错误 : 安全漏洞,未经授权的上传 来源 : https://vercel.com/docs/storage/vercel-blob/client-upload 发生原因 : 在客户端代码中直接使用 BLOB_READ_WRITE_TOKEN 预防措施 : 使用 handleUpload() 生成具有约束条件的客户端特定令牌。
错误 : Error: File size exceeds limit (500MB) 来源 : https://vercel.com/docs/storage/vercel-blob/limits 发生原因 : 上传 >500MB 文件时未使用多部分上传 预防措施 : 上传前验证文件大小,大文件使用多部分上传。
错误 : 浏览器下载文件而不是显示(例如,PDF 以文本形式打开) 来源 : 生产环境调试 发生原因 : 未设置 contentType 选项,Blob 猜测错误 预防措施 : 始终设置 contentType: file.type 或显式 MIME 类型。
错误 : 文件交付缓慢,出口成本高 来源 : Vercel Blob 最佳实践 发生原因 : 对应该公开的文件使用 access: 'private' 预防措施 : 对可公开访问的文件使用 access: 'public'(CDN 缓存)。
错误 : 仅返回前 1000 个文件,文件缺失 来源 : https://vercel.com/docs/storage/vercel-blob/using-blob-sdk#list 发生原因 : 对于大型文件列表,未使用游标进行迭代 预防措施 : 在循环中使用基于游标的分页,直到 cursor 为 undefined。
错误 : 文件未删除,存储配额已满 来源 : https://github.com/vercel/storage/issues/150 发生原因 : 使用错误的 URL 格式,找不到 blob 预防措施 : 使用 put() 响应中的完整 blob URL,检查删除结果。
错误 : 对于 >100MB 的文件(服务器端)出现 Error: Request timeout,或者文件上传在 4.5MB 时失败(无服务器函数限制) 来源 : Vercel 函数超时限制 + 4.5MB 无服务器限制 + 社区讨论 发生原因 :
put() 上传 >4.5MB 的文件会失败。预防措施 : 对于 >4.5MB 的文件,使用客户端上传配合 handleUpload(),或使用多部分上传。
// ❌ 服务器端上传在 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 的文件,使用多部分上传
});
错误 : 文件被覆盖,数据丢失 来源 : 生产环境调试 发生原因 : 多个上传使用相同的文件名 预防措施 : 添加时间戳/UUID:uploads/${Date.now()}-${file.name} 或使用 addRandomSuffix: true。
错误 : 上传完成但应用状态未更新 来源 : https://vercel.com/docs/storage/vercel-blob/client-upload#callback-after-upload 发生原因 : 未实现 onUploadCompleted 回调 预防措施 : 在 handleUpload() 中使用 onUploadCompleted 来更新数据库/状态。
错误 : Error: Access denied, please provide a valid token for this resource 来源 : GitHub Issue #443 发生原因 : 默认令牌在 30 秒后过期。大文件(>100MB)上传时间更长,导致令牌在验证前过期。 预防措施 : 为大文件上传设置 validUntil 参数。
// 对于大文件(>100MB),延长令牌过期时间
const jsonResponse = await handleUpload({
body,
request,
onBeforeGenerateToken: async (pathname) => {
return {
maximumSizeInBytes: 200 * 1024 * 1024,
validUntil: Date.now() + 300000, // 5 分钟
};
},
});
错误 : 当未托管在 Vercel 上时,onUploadCompleted 回调不会触发 来源 : 发布说明 @vercel/blob@2.0.0 发生原因 : v2.0.0 出于安全考虑,移除了从客户端 location.href 自动推断回调 URL 的功能。当不使用 Vercel 系统环境变量时,必须显式提供 callbackUrl。 预防措施 : 对于非 Vercel 托管,在 onBeforeGenerateToken 中显式提供 callbackUrl。
// 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.app
错误 : 在 Firefox 中上传永不完成 来源 : GitHub Issue #881 发生原因 : TypeScript 接口接受 ReadableStream 作为主体类型,但 Firefox 不支持将 ReadableStream 作为 fetch 主体。 预防措施 : 将流转换为 Blob 或 ArrayBuffer 以实现跨浏览器支持。
// ❌ 在 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' });
错误 : 尽管尝试在服务器端覆盖路径名,文件仍上传到错误路径 来源 : GitHub Issue #863 发生原因 : onBeforeGenerateToken 中的 pathname 参数无法更改。它是在客户端调用 upload(pathname, ...) 时设置的。 预防措施 : 在客户端构建路径名,在服务器端验证。使用 clientPayload 传递元数据。
// 客户端:上传前构建路径名
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('Invalid upload path');
}
return {
allowedContentTypes: ['image/jpeg', 'image/png'],
tokenPayload: JSON.stringify({ userId }), // 传递给 onUploadCompleted
};
},
});
错误 : 手动多部分上传因分片过小而失败 来源 : 官方文档 + 社区讨论 发生原因 : 手动多部分上传中的每个分片必须至少为 5MB(最后一个分片除外)。这与 Vercel 的 4.5MB 无服务器函数限制冲突,使得通过服务器端路由进行手动多部分上传变得不可能。 预防措施 : 使用自动多部分上传(put() 中的 multipart: true)或客户端上传。
// ❌ 手动多部分上传失败(无法通过无服务器函数上传 5MB 分片)
const upload = await createMultipartUpload('large.mp4', { access: 'public' });
// uploadPart() 要求至少 5MB - 触及无服务器限制
// ✅ 通过客户端上传使用自动多部分上传
await upload('large.mp4', file, {
access: 'public',
handleUploadUrl: '/api/upload',
multipart: true, // 自动处理 5MB+ 分片
});
错误 : Error: Access denied, please provide a valid token for this resource 来源 : GitHub Issue #664 发生原因 : 没有文件扩展名的路径名会导致描述不清的访问被拒错误。 预防措施 : 始终在路径名中包含文件扩展名。
// ❌ 失败并显示令人困惑的错误
await upload('user-12345', file, {
access: 'public',
handleUploadUrl: '/api/upload',
}); // 错误:访问被拒
// ✅ 提取扩展名并包含在路径名中
const extension = file.name.split('.').pop();
await upload(`user-${userId}.${extension}`, file, {
access: 'public',
handleUploadUrl: '/api/upload',
});
头像上传与替换 :
'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); // 删除旧头像
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'):
const blob = await put(`documents/${userId}/${file.name}`, file, { access: 'private' });
每周安装量
333
代码仓库
GitHub 星标数
643
首次出现
2026年1月20日
已安装于
claude-code271
gemini-cli220
opencode219
cursor211
antigravity202
codex198
Last Updated : 2026-01-21 Version : @vercel/blob@2.0.0 Skill Version : 2.1.0
# Create Blob store: Vercel Dashboard → Storage → Blob
vercel env pull .env.local # Creates BLOB_READ_WRITE_TOKEN
npm install @vercel/blob
Server Upload :
'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 BLOB_READ_WRITE_TOKEN to client. Use handleUpload() for client uploads.
Server Action (generates presigned token):
'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 :
'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
});
List/Delete :
import { list, del } from '@vercel/blob';
// List with pagination
const { blobs, cursor } = await list({ prefix: 'uploads/', cursor });
// Delete
await del(blobUrl);
Multipart ( >500MB):
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 });
Always :
handleUpload() for client uploads (never expose BLOB_READ_WRITE_TOKEN)avatars/, uploads/)Never :
BLOB_READ_WRITE_TOKEN to clientThis skill prevents 16 documented issues :
Error : Error: BLOB_READ_WRITE_TOKEN is not defined Source : https://vercel.com/docs/storage/vercel-blob Why It Happens : Token not set in environment Prevention : Run vercel env pull .env.local and ensure .env.local in .gitignore.
Error : Security vulnerability, unauthorized uploads Source : https://vercel.com/docs/storage/vercel-blob/client-upload Why It Happens : Using BLOB_READ_WRITE_TOKEN directly in client code Prevention : Use handleUpload() to generate client-specific tokens with constraints.
Error : Error: File size exceeds limit (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 : Browser downloads file instead of displaying (e.g., PDF opens as text) Source : Production debugging Why It Happens : Not setting contentType option, Blob guesses incorrectly Prevention : Always set contentType: file.type or explicit MIME type.
Error : Slow file delivery, high egress costs Source : Vercel Blob best practices Why It Happens : Using access: 'private' for files that should be public Prevention : Use access: 'public' for publicly accessible files (CDN caching).
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 cursor is undefined.
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 put() response, check deletion result.
Error : Error: Request timeout 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 :
put() in server actions/API routes fails for files >4.5MB.Prevention : Use client-side upload with handleUpload() for files >4.5MB OR use multipart upload.
// ❌ 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
});
Error : Files overwritten, data loss Source : Production debugging Why It Happens : Using same filename for multiple uploads Prevention : Add timestamp/UUID: uploads/${Date.now()}-${file.name} or addRandomSuffix: true.
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 onUploadCompleted callback Prevention : Use onUploadCompleted in handleUpload() to update database/state.
Error : Error: Access denied, please provide a valid token for this resource 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 validUntil parameter for large file uploads.
// 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
};
},
});
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 location.href for security. When not using Vercel system environment variables, you must explicitly provide callbackUrl. Prevention : Explicitly provide callbackUrl in onBeforeGenerateToken for non-Vercel hosting.
// 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
Error : Upload never completes in Firefox Source : GitHub Issue #881 Why It Happens : The TypeScript interface accepts ReadableStream as a body type, but Firefox does not support ReadableStream as a fetch body. Prevention : Convert stream to Blob or ArrayBuffer for cross-browser support.
// ❌ 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' });
Error : File uploaded to wrong path despite server-side pathname override attempt Source : GitHub Issue #863 Why It Happens : The pathname parameter in onBeforeGenerateToken cannot be changed. It's set at upload(pathname, ...) time on the client side. Prevention : Construct pathname on client, validate on server. Use clientPayload to pass metadata.
// 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
};
},
});
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 (multipart: true in put()) or client uploads.
// ❌ 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
});
Error : Error: Access denied, please provide a valid token for this resource Source : GitHub Issue #664 Why It Happens : Pathname without file extension causes non-descriptive access denied error. Prevention : Always include file extension in pathname.
// ❌ 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',
});
Avatar Upload with Replacement :
'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'):
const blob = await put(`documents/${userId}/${file.name}`, file, { access: 'private' });
Weekly Installs
333
Repository
GitHub Stars
643
First Seen
Jan 20, 2026
Installed on
claude-code271
gemini-cli220
opencode219
cursor211
antigravity202
codex198
React 组合模式指南:Vercel 组件架构最佳实践,提升代码可维护性
106,200 周安装
阿里云DashScope Z-Image Turbo文生图API:快速AI图像生成教程与调用指南
249 周安装
阿里云备份HBR技能:使用OpenAPI和SDK管理云备份资源的完整指南
249 周安装
Spec-Kit技能:基于宪法的规范驱动开发工作流,7阶段GitHub功能开发指南
249 周安装
阿里云AnalyticDB MySQL管理指南:使用OpenAPI与SDK进行云数据库操作
249 周安装
Google App Engine 部署指南:标准与灵活环境配置、Cloud SQL 连接教程
249 周安装
HTMX 开发指南:无需复杂 JavaScript 构建动态 Web 应用 | 现代前端技术
249 周安装