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

Diagrama de las capas que pueden rechazar una carga grande: el cuadro de diálogo de la galería, un proxy inverso, limits.fileSize de multer, el controlador de errores y la comprobación de 4,000,000 bytes en saveImage()

Una foto sacada directamente de un teléfono suele ser más grande que el límite que tenías en mente. En una instalación de Node.js, hasta cinco sitios pueden rechazarla, cada uno con una respuesta distinta. Esta página los recorre en el orden en que los encuentra una solicitud, muestra cómo el ejemplo de Express convierte el límite de multer en un 413 limpio en JSON y explica un comportamiento de multer que decide cuánto de una carga enorme lee tu servidor de todos modos.

Las capas, en orden

  1. El cuadro de diálogo de la galería. Con maxUploadFileSize: 4000000 en las opciones del editor, el cuadro de diálogo rechaza un archivo más grande antes de enviarlo. Un archivo de 4.5 MB dio "big.jpg is 4.3 MB. The maximum allowed size is 3.8 MB." (el cuadro de diálogo cuenta en megabytes binarios), y no se envió ninguna solicitud. Es una comodidad. Cualquiera puede llamar a la URL sin el cuadro de diálogo.
  2. Un proxy inverso, si tienes uno. La documentación de nginx indica que client_max_body_size vale 1m de forma predeterminada, así que detrás de un nginx con la configuración predeterminada una foto de 2 MB nunca llega a Node. nginx responde con su propia página HTML 413, que el cuadro de diálogo indica como una carga fallida.
  3. Los limits de multer, que defines tú. Sus valores predeterminados para fileSize, files, fields y parts son Infinity, y con almacenamiento en memoria eso significa el archivo entero en memoria.
  4. El middleware de errores, que convierte el error de multer en el JSON que espera el navegador.
  5. La propia comprobación del ejemplo en saveImage(), que usa los mismos 4,000,000 bytes y solo importa si alguna vez se aumenta el límite de multer.

Para que el mensaje del ejemplo sea el que vean los usuarios, cada capa anterior debe permitir algo más de 4,000,000 bytes. La codificación multipart añade unos cientos de bytes alrededor del archivo.

El límite de multer

JavaScript · server.mjs
// multer is the multipart parser most Express apps already use. Memory storage keeps the
// file in req.file.buffer, so its bytes can be checked before anything touches the disk;
// the limits keep that buffer small. multer allows exactly fileSize bytes and fails with
// LIMIT_FILE_SIZE above that (turned into a 413 in the error handler below).
const parseForm = multer({
  storage: multer.memoryStorage(),
  limits: { fileSize: MAX_BYTES, files: 1, fields: 10, fieldSize: 1024, parts: 12 },
  defParamCharset: "utf8", // file names are sent as UTF-8; multer's default is latin1
});

fileSize es el tamaño máximo permitido, no el primero que se rechaza. Enviamos un archivo de exactamente 4,000,000 bytes y se guardó. Uno de 4,000,001 bytes se rechazó. Los demás límites están ahí porque el almacenamiento en memoria lo guarda todo: un archivo, diez campos de texto de 1,024 bytes como máximo, doce partes en total. El fieldSize predeterminado de multer es 1 MB por campo de texto.

De MulterError a un 413

Cuando un archivo supera fileSize, multer llama a next(err) con un MulterError. Su code es "LIMIT_FILE_SIZE", su message es "File too large" y su field es "file". Sin un controlador de errores, obtuvimos el comportamiento predeterminado de Express: estado 500 con una página HTML que, a menos que esté definido NODE_ENV=production, incluye el stack trace. El middleware de errores del ejemplo comprueba el código:

JavaScript · server.mjs
// Express 5 sends errors thrown in async handlers here, so the routes need no try/catch.
app.use((err, req, res, next) => {
  if (err instanceof GalleryError) {
    return res.status(err.status).json({ error: err.message });
  }
  if (err instanceof multer.MulterError) {
    const tooBig = err.code === "LIMIT_FILE_SIZE";
    return res
      .status(tooBig ? 413 : 400)
      .json({ error: tooBig ? "The image is larger than 4,000,000 bytes." : "Invalid upload: " + err.message });
  }
  console.error(err);
  res.status(500).json({ error: "Server error." });
});

Cualquier otro error de multer (LIMIT_FILE_COUNT para un segundo archivo, LIMIT_UNEXPECTED_FILE para un archivo con otro nombre de campo) se convierte en un 400 con el mensaje de multer. Comprueba err.code, no el texto del mensaje.

multer lee primero el resto del cuerpo

multer no envía el 413 en el momento en que se supera el límite. Cuando hay un error, lee y descarta el resto de la solicitud antes de llamar a next(err). Un comentario en el código fuente de multer da el motivo: cerrar la conexión mientras el cliente todavía está enviando provoca errores EPIPE en el cliente. Lo medimos: en una carga de 20 MB enviada sin Content-Length, el cliente había enviado los 20,000,000 bytes antes de que llegara el 413. El ejemplo de Next.js, que deja de leer al llegar a su límite, respondió tras unos 4.9 MB.

La memoria está a salvo, porque los bytes de más se descartan. El ancho de banda y el tiempo no: una carga de 1 GB se lee hasta el final. Si eso te importa, rechaza los cuerpos grandes antes de Node, en el proxy inverso, o comprueba req.headers["content-length"] en un middleware antes de multer. Una comprobación de Content-Length por sí sola no cubre las solicitudes chunked, que no llevan longitud.

Pruébalo

Inicia el proyecto de ejemplo de esta guía con npm start, crea un archivo que supere el límite y envíalo:

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

El estado es 413. El archivo son solo ceros, así que también fallaría la comprobación de la firma JPEG, pero el límite de multer llega primero. Para ver la comprobación propia del cuadro de diálogo, elige un archivo de más de 4,000,000 bytes con el botón Upload de la galería. El cuadro de diálogo muestra el mensaje de tamaño y no envía nada.

La misma guía para otras plataformas

Next.js · PHP · ASP.NET Core · MVC 5 · Web Forms

Para seguir leyendo