Límites de tamaño de carga de imágenes en Next.js

Diagrama de las capas que pueden rechazar una carga grande en Next.js: el cuadro de diálogo de la galería, un proxy inverso, request.formData() sin límite, readForm() y la comprobación de 4,000,000 bytes

En un route handler de Next.js, request.formData() es la forma documentada de leer una carga, y nada en Next.js limita lo grande que puede ser esa carga. Esta página muestra lo que medimos, la pequeña función readForm() que el ejemplo usa en su lugar y las demás capas que pueden rechazar una imagen grande antes que tu código.

formData() lo lee todo

Añadimos un route handler temporal que no hacía nada más que await request.formData() y le enviamos un archivo de 50 MB. Respondió 200 con fileSize: 50000000, y la memoria del servidor de desarrollo (RSS) creció unos 175 MB durante esa única solicitud. La documentación de los route handlers describe formData() y no menciona ningún límite de tamaño. Indica que, a diferencia de las rutas de API del Pages Router, los route handlers no necesitan configuración de bodyParser. Así que no hay ningún valor de configuración que establecer. Cualquier límite tiene que estar en tu código.

Cuenta los bytes antes de analizar

readForm(), en lib/gallery.ts, rechaza un Content-Length declarado que supere el límite; si no, lee el flujo del cuerpo y se detiene en cuanto supera el límite. Solo un cuerpo dentro del límite se entrega al analizador estándar, a través de un 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.");
  }
}

El límite es el archivo de 4,000,000 bytes más 64 KB para los campos del formulario y los encabezados multipart que los rodean. La comprobación de Content-Length por sí sola no bastaría: una solicitud chunked no lleva longitud, y enviamos una. El recuento del flujo la detectó: el 413 llegó después de que el cliente hubiera enviado unos 4.9 MB de un cuerpo de 20 MB, sin ningún error de conexión en el cliente, tanto con next dev como con next start. reader.cancel() deja de leer, así que el resto nunca se almacena en búfer. A modo de comparación, Express con multer leyó los 20 MB completos antes de responder.

Después del análisis, saveImage() comprueba el propio archivo. Esa es la comprobación que decide exactamente en 4,000,000 bytes: un archivo de ese tamaño se guardó, y uno de 4,000,001 bytes se rechazó con un 413.

Las demás capas

  1. El cuadro de diálogo de la galería. maxUploadFileSize: 4000000 en las opciones del editor hace que el cuadro de diálogo rechace un archivo más grande antes de enviar nada. Muestra "big.jpg is 4.3 MB. The maximum allowed size is 3.8 MB." El servidor no puede depender de ello.
  2. Un proxy inverso. La documentación de nginx indica que client_max_body_size vale 1m de forma predeterminada. Detrás de un nginx con la configuración predeterminada, una foto de 2 MB recibe la página 413 propia de nginx y nunca llega a Next.js.
  3. Un archivo proxy (lo que antes se llamaba middleware). La documentación de Next.js dice que, cuando se usa uno, Next.js almacena en búfer el cuerpo de la solicitud para que tanto el proxy como la ruta puedan leerlo, hasta experimental.proxyClientMaxBodySize, 10 MB de forma predeterminada. Por encima de eso, el cuerpo se corta en el límite y se registra una advertencia. El ejemplo no tiene archivo proxy, así que no lo probamos. Según esa descripción, si añades uno, un cuerpo de más de 10 MB llega a tu route handler recortado en lugar de rechazado.
  4. Las Server Actions tienen su propio serverActions.bodySizeLimit, 1 MB de forma predeterminada según la documentación. Ese ajuste es para las Server Actions. No afecta a un route handler, y las cargas del editor van a un route handler.

El diagrama del principio muestra el orden. Para que el 413 del ejemplo sea el que reciban los usuarios, cada capa anterior debe permitir algo más de 4,000,000 bytes.

Qué ve el navegador

Cada rechazo del ejemplo es un HTTP 413 con {"error":"The image is larger than 4,000,000 bytes."}. El cuadro de diálogo de la galería muestra su propio texto para cualquier carga fallida, "Upload failed for <name>.", así que con la comprobación de tamaño del cuadro de diálogo activada, los usuarios suelen ver el mensaje del cuadro de diálogo.

Pruébalo

Inicia el proyecto de ejemplo de esta guía con npm run build y npm start, y luego:

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."}

El estado es 413. Para probar el caso chunked, envía el cuerpo sin longitud, por ejemplo con fetch y un cuerpo ReadableStream (duplex: "half"). La respuesta es el mismo 413, y llega antes de que termine la carga.

La misma guía para otras plataformas

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

Para seguir leyendo