Next.js 中的图片上传大小限制

Next.js 中可能拒绝大文件上传的各层示意图:图片库对话框、反向代理、没有限制的 request.formData()、readForm(),以及 4,000,000 字节的检查

在 Next.js 路由处理程序中,request.formData() 是文档推荐的读取上传的方式,而 Next.js 中没有任何机制限制上传的大小。本页介绍我们的实测结果、示例改用的小函数 readForm(),以及可能在您的代码之前就拒绝大图片的其他各层。

formData() 会读取所有内容

我们添加了一个临时路由处理程序,它只执行 await request.formData(),然后向它发送了一个 50 MB 的文件。它返回了 200,fileSize: 50000000,而在这一个请求期间,开发服务器的内存(RSS)增长了约 175 MB。路由处理程序文档介绍了 formData(),但没有提到任何大小限制。文档指出,与 Pages Router 的 API 路由不同,路由处理程序不需要配置 bodyParser。因此没有可以设置的配置值,任何限制都必须写在您的代码中。

在解析之前计算字节数

lib/gallery.ts 中的 readForm() 会拒绝声明的 Content-Length 超过限制的请求,否则就读取请求体流,并在超过限制时立即停止。只有不超过限制的请求体才会通过一个 Response 交给标准解析器:

TypeScript · lib/gallery.ts
// request.formData() reads the whole body into memory, however large. This reads at most
// MAX_BYTES plus room for the other fields, answers 413 above that, then parses it.
export async function readForm(request: Request): Promise<FormData> {
  const limit = MAX_BYTES + 64 * 1024;
  if (Number(request.headers.get("content-length")) > limit) {
    throw new GalleryError(413, TOO_BIG);
  }
  const chunks: Uint8Array[] = [];
  let size = 0;
  const reader = request.body?.getReader();
  while (reader) {
    const { done, value } = await reader.read();
    if (done) break;
    size += value.length;
    if (size > limit) {
      await reader.cancel();
      throw new GalleryError(413, TOO_BIG);
    }
    chunks.push(value);
  }
  const body = new Response(Buffer.concat(chunks), {
    headers: { "content-type": request.headers.get("content-type") ?? "" },
  });
  try {
    return await body.formData();
  } catch {
    throw new GalleryError(400, "Expected a multipart/form-data request.");
  }
}

限制值为 4,000,000 字节的文件大小,再加上 64 KB,用于容纳表单字段及其周围的 multipart 标头。仅检查 Content-Length 是不够的:分块传输的请求不带长度,我们就发送过这样的请求。流式计数拦住了它:在 next dev 和 next start 中,客户端发送了 20 MB 请求体中的约 4.9 MB 后,413 就返回了,客户端也没有出现连接错误。reader.cancel() 会停止读取,因此剩余部分永远不会被缓冲。作为对比,使用 multer 的 Express 读完了全部 20 MB 才作出响应。

解析之后,saveImage() 会检查文件本身。正是这项检查以精确的 4,000,000 字节为界:这个大小的文件被保存了,而 4,000,001 字节的文件以 413 被拒绝。

其他各层

  1. 图片库对话框。编辑器选项中的 maxUploadFileSize: 4000000 让对话框在发送任何内容之前就拒绝更大的文件,并显示 "big.jpg is 4.3 MB. The maximum allowed size is 3.8 MB."。服务器不能依赖这一层。
  2. 反向代理。根据 nginx 文档,client_max_body_size 的默认值为 1m。在默认配置的 nginx 之后,2 MB 的照片会得到 nginx 自己的 413 页面,根本到不了 Next.js。
  3. proxy 文件(以前称为 middleware)。Next.js 文档说明,使用它时,Next.js 会缓冲请求体,以便代理和路由都能读取,上限为 experimental.proxyClientMaxBodySize,默认 10 MB。超过这个值时,请求体会在限制处被截断,并记录一条警告。示例没有 proxy 文件,因此我们没有测试这种情况。按照这一描述,如果您添加了 proxy 文件,超过 10 MB 的请求体会被截断后送到路由处理程序,而不是被拒绝。
  4. Server Actions 有自己的 serverActions.bodySizeLimit,根据文档默认为 1 MB。这个设置只针对 Server Actions,对路由处理程序不起作用,而编辑器的上传发送到的是路由处理程序。

顶部的示意图展示了这些层的顺序。要让用户得到的是示例返回的 413,前面的每一层都必须允许略多于 4,000,000 字节的数据。

浏览器看到什么

示例的每一次拒绝都是带有 {"error":"The image is larger than 4,000,000 bytes."} 的 HTTP 413。对于任何失败的上传,图片库对话框都显示自己的文本 "Upload failed for <name>.",因此在启用对话框自身的大小检查时,用户通常看到的是对话框的消息。

测试

用 npm run build 和 npm start 启动本指南的示例项目,然后执行:

Shell
head -c 4500000 /dev/zero > big.jpg
curl -F "file=@big.jpg" http://localhost:5102/rte/upload
{"error":"The image is larger than 4,000,000 bytes."}

状态码为 413。要尝试分块传输的情况,请发送不带长度的请求体,例如使用 fetch 和 ReadableStream 请求体(duplex: "half")。响应同样是 413,而且会在上传完成之前到达。

其他平台的同一指南

JavaScript · PHP · ASP.NET Core · MVC 5 · Web Forms

延伸阅读