Multer: 413 File too large con un archivo en el límite

Tu interfaz anuncia "máximo 25 MB por archivo", pero al subir un archivo de exactamente 25 MB el servidor responde con un error 413. Un archivo un byte más pequeño pasa sin problema. En NestJS, con FileInterceptor o FilesInterceptor, la respuesta se ve así:

HTTP/1.1 413 Payload Too Large

{"statusCode":413,"message":"File too large","error":"Payload Too Large"}

El formato exacto del cuerpo depende de la versión de NestJS. Lo mismo ocurre con cualquier otro valor: con un límite de 10 MB, un archivo de exactamente 10 MB es rechazado. Este artículo explica el motivo, cómo corregirlo sin dejar el límite impreciso y cómo probar el borde exacto desde el navegador.

Por qué pasa

NestJS usa Multer bajo FileInterceptor, y Multer usa busboy para leer el cuerpo multipart/form-data. La documentación de Multer describe limits.fileSize como el tamaño máximo del archivo en bytes, pero no dice si el valor límite se acepta o se rechaza. Para responder esa pregunta hay que mirar el código:

  • busboy limita los bytes que entrega a fileSizeLimit - fileSize y emite el evento limit en cuanto el contador llega al tope, no cuando lo supera. Marca además el flujo del archivo como truncado.
  • Multer escucha ese evento en el flujo del archivo y aborta la petición con el código LIMIT_FILE_SIZE, cuyo mensaje es "File too large".
  • NestJS traduce LIMIT_FILE_SIZE a PayloadTooLargeException, que es el 413.

El resultado práctico, que coincide con lo observado: con fileSize = N, un archivo de exactamente N bytes dispara el evento y se rechaza, así que el máximo aceptado efectivo es N − 1. Esto se deduce del código fuente de busboy y Multer en sus ramas principales, no de una frase de la documentación; por eso más abajo se incluye una prueba para comprobarlo en tu propia versión.

Solución

  1. Define el límite de negocio una sola vez, en bytes y con la unidad clara. Aquí "25 MB" significa 25 × 1024 × 1024 bytes; si tu producto usa megabytes decimales (25 000 000), cambia la constante, pero usa una sola definición para interfaz, validación y pruebas:

    export const MAX_FILE_BYTES = 25 * 1024 * 1024;
  2. Pasa MAX_FILE_BYTES + 1 a Multer. Así un archivo de exactamente MAX_FILE_BYTES bytes queda por debajo del tope y se acepta, y uno de un byte más se rechaza:

    import { Controller, Post, UploadedFile, UseInterceptors } from '@nestjs/common';
    import { FileInterceptor } from '@nestjs/platform-express';
    import { ArchivosService } from './archivos.service';
    import { MAX_FILE_BYTES } from './limites';
    
    @Controller('archivos')
    export class ArchivosController {
      constructor(private readonly archivos: ArchivosService) {}
    
      @Post()
      @UseInterceptors(
        FileInterceptor('file', {
          // Multer rechaza un archivo cuyo tamaño ALCANZA el límite,
          // así que se suma 1 para aceptar exactamente MAX_FILE_BYTES.
          limits: { fileSize: MAX_FILE_BYTES + 1 },
        }),
      )
      subir(@UploadedFile() file: Express.Multer.File) {
        return this.archivos.guardar(file);
      }
    }
  3. Mantén la validación de negocio en el servicio, con el mismo umbral y un mensaje claro. El 413 genérico de Multer dice "File too large" sin indicar el máximo; tu regla puede decirlo, y queda protegida si alguien cambia después la configuración del interceptor:

    import { Injectable, PayloadTooLargeException } from '@nestjs/common';
    import { MAX_FILE_BYTES } from './limites';
    
    @Injectable()
    export class ArchivosService {
      guardar(file: Express.Multer.File) {
        if (file.size > MAX_FILE_BYTES) {
          throw new PayloadTooLargeException('El archivo supera el máximo de 25 MB.');
        }
        // ...guardar el archivo
      }
    }
  4. Prueba el borde exacto con archivos sintéticos desde la consola del navegador, sin buscar un archivo del tamaño justo. El constructor File acepta un Uint8Array como contenido, y fetch con un FormData como cuerpo establece por sí mismo el encabezado Content-Type con el delimitador correcto; no lo escribas a mano, porque perderías ese delimitador:

    const MAX = 25 * 1024 * 1024;
    
    async function probar(bytes) {
      const datos = new FormData();
      datos.append('file', new File([new Uint8Array(bytes)], 'prueba.bin'));
      const respuesta = await fetch('/archivos', { method: 'POST', body: datos, credentials: 'include' });
      console.log(bytes, respuesta.status);
    }
    
    await probar(MAX - 1);
    await probar(MAX);
    await probar(MAX + 1);
    await probar(MAX + 5 * 1024);

    Ajusta la URL y la autenticación a tu API. Antes de la corrección, el resultado típico es éxito, 413, 413 y 413; después, éxito, éxito, 413 y 413.

Cómo verificar que quedó resuelto

Ejecuta la función probar de arriba y revisa los códigos de estado:

  • MAX - 1 y MAX deben responder con éxito (en este ejemplo, 201).
  • MAX + 1 y MAX + 5 * 1024 deben seguir respondiendo 413.
  • Si tu endpoint usa FilesInterceptor, una subida de varios archivos grandes, por ejemplo cuatro de 10 MB cada uno, debe completarse con normalidad, porque fileSize se evalúa archivo por archivo.

En un caso real, tras el cambio un archivo de exactamente 25 MB pasó de 413 a 201, uno de 25 MB más 5 KB siguió dando 413 y cuatro archivos de 10 MB se subieron sin problemas. Añade además una prueba automatizada del servicio con un archivo de MAX_FILE_BYTES y otro de MAX_FILE_BYTES + 1 bytes para que el borde no vuelva a romperse.

Variantes y errores relacionados

413 con archivos pequeños: probablemente no es Multer

Si el 413 aparece con archivos de pocos megabytes, sospecha de un proxy inverso delante de tu aplicación. En nginx, la directiva client_max_body_size limita el tamaño del cuerpo de la petición; su valor por defecto es 1m, y cuando se supera el servidor responde con el error 413. Una pista útil: la respuesta de nginx suele ser una página HTML, mientras que la de NestJS es JSON con el texto "File too large". Es una hipótesis que debes comprobar en tu infraestructura, no la causa de este artículo.

server {
    client_max_body_size 30m;
}

MaxFileSizeValidator tiene el mismo borde

Si validas con ParseFilePipe y MaxFileSizeValidator, ten en cuenta que, según la documentación de NestJS, comprueba que el tamaño sea menor que el valor indicado. En su código fuente la comparación es estricta (file.size < maxSize), así que un archivo exactamente igual al máximo también falla, aunque ahora con un 400 y su propio mensaje. Usa maxSize: MAX_FILE_BYTES + 1 si quieres aceptar el borde.

Otros códigos de Multer

El código LIMIT_UNEXPECTED_FILE aparece cuando el nombre del campo del archivo no coincide con el que declaraste en FileInterceptor('file'); NestJS lo traduce a un 400, no a un 413. Si usas Express sin NestJS, puedes capturar el error con err instanceof multer.MulterError y comprobar err.code === 'LIMIT_FILE_SIZE' para devolver tu propio mensaje.

Valida también en el cliente

Comprobar file.size > MAX en el navegador evita enviar archivos que seguro fallarán y permite mostrar el mensaje antes de la subida. La validación del servidor sigue siendo la que manda.

Resumen

  • Con limits.fileSize = N, un archivo de exactamente N bytes recibe LIMIT_FILE_SIZE y NestJS responde 413; el máximo efectivo es N − 1.
  • La documentación de Multer solo habla de "tamaño máximo"; el borde exacto se deduce del código de busboy y de Multer y conviene probarlo en tu versión.
  • Corrige con limits: { fileSize: MAX_FILE_BYTES + 1 } y conserva la validación file.size > MAX_FILE_BYTES en el servicio con un mensaje claro.
  • Prueba MAX - 1, MAX, MAX + 1 y un valor mayor con new File([new Uint8Array(n)], ...) y fetch.
  • Un 413 con archivos pequeños suele venir de un proxy (por ejemplo, el 1m por defecto de nginx), no de Multer.

Referencias

Comentarios