Limites de tamanho para upload de imagens em Next.js

Diagrama das camadas que podem recusar um upload grande em Next.js: a caixa de diálogo da galeria, um proxy reverso, request.formData() sem limite, readForm() e a verificação de 4,000,000 bytes

Em um route handler do Next.js, request.formData() é a forma documentada de ler um upload, e nada no Next.js limita o tamanho desse upload. Esta página mostra o que medimos, a pequena função readForm() que o exemplo usa no lugar dela e as outras camadas que podem recusar uma imagem grande antes do seu código.

formData() lê tudo

Adicionamos um route handler temporário que não fazia nada além de await request.formData() e enviamos a ele um arquivo de 50 MB. Ele respondeu 200 com fileSize: 50000000, e a memória (RSS) do servidor de desenvolvimento cresceu cerca de 175 MB durante essa única requisição. A documentação de route handlers descreve formData() e não menciona nenhum limite de tamanho. Ela observa que, ao contrário das API routes do Pages Router, os route handlers não precisam de configuração de bodyParser. Portanto, não há valor de configuração a definir. Qualquer limite precisa estar no seu código.

Conte os bytes antes de fazer o parsing

readForm(), em lib/gallery.ts, recusa um Content-Length declarado acima do limite; caso contrário, lê o stream do corpo e para assim que ele ultrapassa o limite. Apenas um corpo dentro do limite é entregue ao parser padrão, por meio de um 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.");
  }
}

O limite é o arquivo de 4,000,000 bytes mais 64 KB para os campos do formulário e os cabeçalhos multipart ao redor deles. Só a verificação de Content-Length não bastaria: uma requisição chunked não traz tamanho, e nós enviamos uma. A contagem no stream a pegou: o 413 voltou depois que o cliente havia enviado cerca de 4.9 MB de um corpo de 20 MB, sem erro de conexão no lado do cliente, tanto no next dev quanto no next start. reader.cancel() interrompe a leitura, então o restante nunca vai para o buffer. Para comparar, o Express com multer leu os 20 MB inteiros antes de responder.

Depois do parsing, saveImage() verifica o próprio arquivo. É essa verificação que decide exatamente nos 4,000,000 bytes: um arquivo desse tamanho foi armazenado, e um de 4,000,001 bytes foi recusado com 413.

As outras camadas

  1. A caixa de diálogo da galeria. maxUploadFileSize: 4000000 nas opções do editor faz a caixa de diálogo recusar um arquivo maior antes de enviar qualquer coisa. Ela mostra "big.jpg is 4.3 MB. The maximum allowed size is 3.8 MB." O servidor não pode depender disso.
  2. Um proxy reverso. A documentação do nginx dá a client_max_body_size o padrão de 1m. Atrás de um nginx com configuração padrão, uma foto de 2 MB recebe a própria página 413 do nginx e nunca chega ao Next.js.
  3. Um arquivo proxy (o que antes se chamava middleware). A documentação do Next.js diz que, quando ele é usado, o Next.js mantém o corpo da requisição em buffer para que tanto o proxy quanto a rota possam lê-lo, até experimental.proxyClientMaxBodySize, 10 MB por padrão. Acima disso, o corpo é cortado no limite e um aviso é registrado no log. O exemplo não tem arquivo proxy, então não testamos isso. Por essa descrição, se você adicionar um, um corpo acima de 10 MB chega ao seu route handler truncado, em vez de recusado.
  4. As Server Actions têm seu próprio serverActions.bodySizeLimit, 1 MB por padrão segundo a documentação. Essa configuração é para Server Actions. Ela não tem efeito em um route handler, e os uploads do editor vão para um route handler.

O diagrama no topo mostra a ordem. Para que o 413 do exemplo seja o que as pessoas recebem, cada camada anterior precisa permitir um pouco mais de 4,000,000 bytes.

O que o navegador vê

Toda recusa do exemplo é um HTTP 413 com {"error":"The image is larger than 4,000,000 bytes."}. A caixa de diálogo da galeria mostra seu próprio texto para qualquer upload que falhe, “Upload failed for <name>.”, então, com a verificação de tamanho da própria caixa de diálogo ativada, as pessoas geralmente veem a mensagem da caixa de diálogo.

Teste

Inicie o projeto de exemplo deste guia com npm run build e npm start e, depois:

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

O status é 413. Para testar o caso chunked, envie o corpo sem tamanho, por exemplo com fetch e um corpo ReadableStream (duplex: "half"). A resposta é o mesmo 413, e ela chega antes de o upload terminar.

O mesmo guia para outras plataformas

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

Leia a seguir