我正在使用 Vercel Serverless 函数来处理上传到 Digital Ocean Spaces 的文件(与 AWS S3 相同的 API)。但是,我在请求处理程序中处理多部分/表单数据时遇到了问题。
在前端,我使用 fetch 来发布带有文件和几个文本字段的 FormData() 对象。当在标题上记录主体和服务器时,我可以按预期看到那里的所有内容,但是当使用 Multer 处理多部分时(我也尝试了其他几个类似的包),我无法检索任何已发布的字段或文件.
还值得注意的是,当使用 Postman 测试 POST 请求时,我遇到了完全相同的问题,所以我确信问题出在无服务器功能上。
const handleSubmit = async (values) => {
const formData = new FormData();
// build my Form Data from state.
Object.keys(values).forEach(key => {
formData.append(key, values[key]);
});
const response = await fetch("/api/post-submission", {
method: "POST",
headers: {
Accept: "application/json",
},
body: formData,
});
const json = await response.json();
};
const util = require("util");
const multer = require("multer");
module.exports = async (req, res) => {
await util.promisify(multer().any())(req, res);
console.log("req.body", req.body); // >> req.body [Object: null prototype] {}
console.log("req.files", req.files); // >> req.files []
// Do the file upload to S3...
res.status(200).json({ uploadData });
};
req.body 和 req.files 应该填充我提交的数据。
您可以使用 multiparty 包来解析 multipart/form-data。关键是还要导出一个
config
对象来关闭 bodyParser。这将允许多方按设计工作并防止可怕的 stream ended unexpectedly
错误。
下面的代码是上传 api 页面的完整工作示例。
import { NextApiRequest, NextApiResponse } from "next";
import multiparty from "multiparty";
const uploadImage = async (req: NextApiRequest, res: NextApiResponse) => {
const form = new multiparty.Form();
const data = await new Promise((resolve, reject) => {
form.parse(req, function (err, fields, files) {
if (err) reject({ err });
resolve({ fields, files });
});
});
console.log(`data: `, JSON.stringify(data));
res.status(200).json({ success: true });
};
export default uploadImage;
export const config = {
api: {
bodyParser: false,
},
};
我不太确定 Multer 包,但没有任何固有限制阻止 Vercel 上的无服务器函数(底层是 AWS Lambda)处理多部分/表单数据。
let multiparty = require('multiparty')
let http = require('http')
let util = require('util')
module.exports = (req, res) => {
if (req.method === "POST") {
let form = new multiparty.Form();
form.parse(req, (err, fields, files) => {
res.writeHead(200, { 'content-type': 'text/plain' });
res.write('received upload: \n\n');
res.end(util.inspect({ fields: fields, files: files }));
});
return;
} else {
res.writeHead(405, { 'content-type': 'text/plain' });
res.end("Method not allowed. Send a POST request.");
return;
}
}
我使用部署的 URL 创建了一个演示存储库此处
我能够通过使用
busboy使
multipart/form-data
工作。
const Busboy = require('busboy');
module.exports = (req, res) => {
const busboy = new Busboy({ headers: req.headers });
busboy.on('file', function(fieldname, file, filename, encoding, mimetype) {
console.log('File [' + fieldname + ']: filename: ' + filename);
file.on('data', function(data) {
console.log('File [' + fieldname + '] got ' + data.length + ' bytes');
});
file.on('end', function() {
console.log('File [' + fieldname + '] Finished');
});
});
busboy.on('field', function(fieldname, val, fieldnameTruncated, valTruncated) {
console.log('Field [' + fieldname + ']: value: ' + val);
});
busboy.on('finish', function() {
console.log('Done parsing form!');
res.writeHead(303, { Connection: 'close', Location: '/' });
res.end();
});
req.pipe(busboy);
}
嗨,这可以在没有任何库的情况下实现,Next.js 13 已经支持
FormData
!
这是我在项目中使用的代码,它一定会对你有帮助。
上下文:我实现的功能是允许用户更新他们的个人资料图片。
// In your React client component
const [file, setFile] = useState<File | null>(null)
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault()
if (!file) throw new Error('No file selected')
const { imageUrl } = await uploadProfileImage(file)
console.log('New image URL', imageUrl)
}
const uploadProfileImage = async (file: File) => {
const body = new FormData()
body.set('image', file)
const response = await fetch('/api/upload/profile-image', {
method: 'POST',
body,
})
if (!response.ok) {
throw new Error('Error uploading profile image')
}
const result: UploadProfileImageResponse = await response.json()
if (!result) throw new Error('Error uploading profile image')
return result
}
// In `src/app/api/upload/profile-image/route.ts`
import sharp from 'sharp'
import { uploadToS3 } from '~/server/aws'
import { db } from '~/server/db/db'
import { eq } from 'drizzle-orm'
import { users } from '~/server/db/schema'
import { NextRequest, NextResponse } from 'next/server'
import { getServerSession } from 'next-auth'
import { authOptions } from '~/server/auth'
export async function POST(request: NextRequest) {
// Step 1: Check if user is authenticated (With NextAuth)
const session = await getServerSession(authOptions)
if (!session) {
return NextResponse.json(null, { status: 401 })
}
// Step 2: Get image from request (With Next.js API Routes)
const formData = await request.formData()
const imageFile = formData.get('image') as unknown as File | null
if (!imageFile) {
return NextResponse.json(null, { status: 400 })
}
const imageBuffer = Buffer.from(await imageFile.arrayBuffer())
// Step 3: Resize image (With Sharp)
const editedImageBuffer = await sharp(imageBuffer)
.resize({ height: 256, width: 256, fit: 'cover' })
.toBuffer()
// Step 4: Upload image (With AWS SDK)
const imageUrl = await uploadToS3({
buffer: editedImageBuffer,
key: `profile-images/${session.user.id}`,
contentType: imageFile.type,
})
// Step 5: Update user in database (With Drizzle ORM)
await db
.update(users)
.set({
image: imageUrl,
})
.where(eq(users.id, session.user.id))
// Step 6: Return new image URL
return NextResponse.json({ imageUrl })
}
// Export types for API Routes
export type UploadProfileImageResponse = ExtractGenericFromNextResponse<
Awaited<ReturnType<typeof POST>>
>
type ExtractGenericFromNextResponse<Type> = Type extends NextResponse<infer X>
? X
: never
// In `src/server/aws.ts`
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'
export const BUCKET_REGION = 'my-bucket-region'
export const BUCKET_NAME = 'my-bucket-name'
export const s3 = new S3Client({
region: BUCKET_REGION,
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID ?? 'missing-env',
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY ?? 'missing-env',
},
})
export async function uploadToS3<K extends string>({
buffer,
key,
contentType,
}: {
buffer: Buffer
key: K
contentType: string
}) {
await s3.send(
new PutObjectCommand({
Bucket: BUCKET_NAME,
Key: key,
Body: buffer,
ContentType: contentType,
})
)
return `https://${BUCKET_NAME}.s3.${BUCKET_REGION}.amazonaws.com/${key}` as const
}
我看到你使用form-data,你可以将标题内容类型设置为multipart/form-data;boundary=----WebKitFormBoundaryyrV7KO0BoCBuDbTL
vercel 服务上的无服务器功能服务器需要一个包含路由的配置文件 (vercel.json) 才能工作,如果您需要了解路由如何工作:https://vercel.com/docs/configuration#project/routes
配置文件将帮助您将传入的 post 请求重定向到 js 文件。例如:
{
"routes" : [
{"src":"/api/post-submission", "methods": ["POST"], "dest": "<js file>"}
]
}