将多部分/表单数据发布到无服务器 Next.js API(在 Vercel / Now.sh 上运行)

问题描述 投票:0回答:6

我正在使用 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 应该填充我提交的数据。

node.js forms multipartform-data serverless vercel
6个回答
10
投票

2022 更新

您可以使用 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,
  },
};

4
投票

我不太确定 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 创建了一个演示存储库此处


3
投票

我能够通过使用

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);
}

0
投票

嗨,这可以在没有任何库的情况下实现,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
}

-1
投票

我看到你使用form-data,你可以将标题内容类型设置为multipart/form-data;boundary=----WebKitFormBoundaryyrV7KO0BoCBuDbTL


-3
投票

vercel 服务上的无服务器功能服务器需要一个包含路由的配置文件 (vercel.json) 才能工作,如果您需要了解路由如何工作:https://vercel.com/docs/configuration#project/routes

配置文件将帮助您将传入的 post 请求重定向到 js 文件。例如:

{
  "routes" : [
    {"src":"/api/post-submission", "methods": ["POST"], "dest": "<js file>"}
  ]
}
© www.soinside.com 2019 - 2024. All rights reserved.