Home > Blog > Desenvolvimento Web
Desenvolvimento Web
JavaScript
Programação

Multipart Upload no Node.js

Atualizado em: 31 de agosto de 2026

Estação de trabalho usada no desenvolvimento de aplicações Node.js

Implementar Multipart Upload no Node.js permite receber arquivos e campos enviados com multipart/form-data. Esse formato é comum em formulários HTML, APIs de upload, importação de documentos e envio de imagens.

O principal cuidado é evitar carregar o arquivo inteiro em memória ou confiar no nome, extensão e Content-Type enviados pelo cliente. Uma rota segura usa parser em streaming, limites de tamanho e quantidade, nomes gerados pelo servidor, validação do formato real, armazenamento temporário controlado e limpeza em caso de erro.

Neste guia, você aprenderá como funciona multipart/form-data, usar Busboy ou integração do framework, processar streams, limitar partes, enviar para S3 ou MinIO, validar arquivos, proteger contra DoS e testar o fluxo.

O que é multipart/form-data?

O formato divide o corpo em partes separadas por um boundary. A RFC 7578 descreve multipart/form-data. A documentação do Busboy mostra um parser streaming para Node.js.

Exemplo de requisição

Content-Type: multipart/form-data; boundary=abc123

--abc123
Content-Disposition: form-data; name="title"

Relatório
--abc123
Content-Disposition: form-data; name="file"; filename="report.pdf"
Content-Type: application/pdf

...bytes...
--abc123--

Boundary

O boundary é definido no header e separa as partes. Não tente interpretar manualmente com split(), porque o conteúdo é binário, pode chegar em chunks e o boundary pode cruzar limites de buffer.

Não use express.json()

O parser JSON não entende multipart. Configure a rota com um parser específico e evite consumir o stream antes dele.

Busboy

npm install busboy

Parser básico

import Busboy from 'busboy';

app.post('/uploads', (req, res, next) => {
  const busboy = Busboy({
    headers: req.headers,
    limits: {
      files: 1,
      fields: 10,
      fileSize: 10 * 1024 * 1024,
      parts: 12
    }
  });

  req.pipe(busboy);
});

Evento file

busboy.on('file', (
  fieldName,
  stream,
  info
) => {
  const { filename, encoding, mimeType } = info;
});

filename e mimeType são controlados pelo cliente.

Evento field

busboy.on('field', (name, value, info) => {
  fields[name] = value;
});

Limite tamanho e quantidade de campos. Um formulário pode enviar milhares de entradas.

Streams

O arquivo chega como Readable stream. Você pode enviar diretamente ao object storage, antivírus ou arquivo temporário.

Consulte Streams no Node.js.

Pipeline

import { pipeline } from 'node:stream/promises';

await pipeline(
  uploadStream,
  validationTransform,
  destinationStream
);

pipeline() propaga erros e encerra streams corretamente.

Backpressure

Não use listeners de data que acumulam todos os chunks sem necessidade. Pipeline e pipe respeitam o ritmo do destino.

Limite de arquivo

O parser deve interromper o stream depois do máximo. Também verifique eventos ou flags de truncamento da biblioteca.

Evento limit

stream.on('limit', () => {
  uploadTooLarge = true;
});

Não marque o upload como concluído quando foi truncado.

Limite total da requisição

Além do limite por arquivo, configure limite total no proxy e na aplicação. Vários arquivos pequenos podem exceder memória e tempo.

Quantidade de arquivos

Defina files, parts e fields. Rejeite partes adicionais.

Nome do arquivo

Nunca use diretamente:

fs.createWriteStream(`/uploads/${filename}`)

O nome pode conter separadores, caracteres especiais e colisões.

Nome gerado

const objectId = crypto.randomUUID();
const objectKey = `tenants/${tenantId}/uploads/${objectId}`;

Guarde o nome original apenas como metadado sanitizado.

Path traversal

Mesmo com basename(), é melhor não usar o nome como path. Um ID gerado elimina a classe de problema.

Content-Type

O header pode mentir. Detecte magic bytes com biblioteca mantida ou parser do formato.

Magic bytes

PDF costuma começar com assinatura específica, e imagens possuem headers próprios. Leia apenas os primeiros bytes necessários e depois devolva-os ao stream.

Peek no stream

Use um transform que armazena um prefixo pequeno para identificação, sem consumir o restante.

Extensão

Determine a extensão final a partir do formato detectado, não do nome enviado.

SVG

SVG é XML e pode conter scripts ou referências externas. Se a aplicação não precisa, rejeite. Se precisa, sanitize e sirva em domínio separado com headers adequados.

HTML

Não sirva upload HTML inline na mesma origem da aplicação. Use Content-Disposition: attachment e origem isolada.

Imagens

Reencode imagens para remover payloads, metadata e formatos inesperados. Limite dimensões para evitar decompression bombs.

PDF

PDF pode conter scripts, anexos e links. Use scanner e processamento isolado conforme o risco.

Antivírus

Novos arquivos podem ir para quarentena. Um worker analisa e altera o status para aprovado ou rejeitado.

Não disponibilizar antes da análise

O banco deve registrar:

pending | scanning | approved | rejected

Rotas de download permitem apenas approved.

Armazenamento temporário

Quando precisar de arquivo local, use diretório específico, permissões restritas e nome aleatório.

const tempPath = join(
  tempDirectory,
  crypto.randomUUID()
);

Limpeza

Use finally para remover temporários. Um job periódico deve apagar arquivos órfãos antigos.

Disco cheio

Monitore espaço e defina quota. Um atacante pode preencher o volume temporário.

Upload direto para S3

Arquivos grandes podem ir diretamente para object storage com URL pré-assinada. Consulte Uploads para S3 no Node.js.

MinIO

Em infraestrutura própria, use MinIO no Node.js.

Proxy via API

O servidor pode encaminhar o stream para S3, mas isso consome banda e conexão da API. Use quando precisa validar durante a transferência ou esconder o storage.

Presigned upload

A API cria uma key e política, o navegador envia diretamente e depois confirma. Valide o objeto com HeadObject ou statObject.

Campos antes do arquivo

A ordem das partes não é garantida. Não dependa de receber tenantId ou metadata antes do arquivo.

Tenant

O tenant deve vir da autenticação, não do formulário. Consulte Multi-Tenancy no Node.js.

Autorização

Verifique permission antes de iniciar o upload. Um usuário autenticado não pode enviar para qualquer projeto.

CSRF

Se a autenticação usa cookies, proteja a rota contra CSRF. Uploads também alteram estado e consomem recursos.

Rate limiting

Limite por usuário, tenant, IP confiável e quantidade de bytes. Consulte Rate Limiting no Node.js.

Quota

Controle armazenamento total, uploads por dia, tamanho por arquivo e quantidade pendente.

Timeouts

Defina timeout para headers, corpo e processamento. Conexões lentas podem manter recursos ocupados.

Slowloris

Um cliente pode enviar bytes muito lentamente. O proxy e o servidor devem ter limites de tempo e taxa mínima quando apropriado.

AbortController

Quando o cliente desconecta, cancele upload ao storage e processamento.

req.on('aborted', () => {
  controller.abort();
});

Erro parcial

Se o objeto foi criado, mas o banco falhou, marque para limpeza. Se o banco registrou e o storage falhou, mantenha status de erro recuperável.

Outbox

Para iniciar scanner ou processamento, grave uma mensagem de outbox junto ao registro do upload. Consulte Outbox Pattern no Node.js.

Idempotência

Um retry do cliente não deve criar vários objetos quando representa o mesmo upload. Use upload ID.

Veja Idempotência em APIs Node.js.

FormData no cliente

O cliente pode construir o formulário com FormData. Consulte FormData no Node.js.

Não definir boundary manualmente

Ao usar FormData, deixe a biblioteca definir o Content-Type com boundary. Um header manual incorreto quebra o parser.

Múltiplos arquivos

Processe sequencialmente ou com concorrência limitada. Não envie todos simultaneamente para o storage sem controle.

Transações

Object storage e banco não compartilham transação. Modele estados e compensações.

Resposta

{
  "uploadId": "...",
  "status": "scanning",
  "filename": "report.pdf"
}

Não retorne path interno ou credenciais.

Logs

Registre upload ID, tenant, tamanho, tipo detectado, duração e status. Não registre conteúdo ou URL pré-assinada completa.

Consulte Logs com Pino no Node.js.

Auditoria

Uploads sensíveis, downloads, rejeições e exclusões devem ser auditados. Veja Logs de Auditoria no Node.js.

Métricas

Monitore bytes, duração, truncamentos, tipos rejeitados, scans, objetos órfãos, disco temporário e desconexões.

Teste de arquivo pequeno

test('recebe arquivo válido', async () => {
  const response = await request(app)
    .post('/uploads')
    .attach('file', fixturePath)
    .field('title', 'Relatório');

  assert.equal(response.status, 202);
});

Teste de tamanho

Envie um byte acima do limite e confirme rejeição e limpeza do objeto parcial.

Teste de nome malicioso

Use ../../arquivo, Unicode e nome enorme. O path final deve continuar gerado pelo servidor.

Teste de tipo falso

Envie HTML declarado como imagem e confirme detecção.

Teste de desconexão

Aborte o cliente no meio do stream e confirme cancelamento e limpeza.

Teste de várias partes

Exceda quantidade de fields, files e parts. O parser deve interromper.

Teste de ordem

Envie arquivo antes dos campos e confirme que o fluxo não depende da ordem.

Teste de tenant

Um usuário do tenant A não pode confirmar upload pertencente ao tenant B.

Erros comuns

  • Carregar tudo em memória: arquivos causam DoS.
  • Usar filename como path: traversal e colisão.
  • Confiar no MIME: arquivo malicioso passa.
  • Sem limits: partes ilimitadas consomem recursos.
  • Ignorar truncamento: arquivo incompleto é aceito.
  • Não limpar temporários: disco enche.
  • Disponibilizar antes do scan: malware é servido.

Boas práticas

  • Use parser streaming.
  • Defina todos os limits.
  • Gere nomes no servidor.
  • Detecte formato real.
  • Use pipeline e backpressure.
  • Armazene em quarentena.
  • Limpe em finally.
  • Cancele na desconexão.
  • Aplique quota e rate limit.
  • Teste payloads maliciosos.

Conclusão

Implementar Multipart Upload no Node.js exige processar cada parte como stream e impor limites antes de consumir recursos. Filename e Content-Type são apenas informações fornecidas pelo cliente.

Com IDs gerados, validação real, quarentena, cancelamento e object storage, o upload se torna previsível e escalável. Testes de truncamento, desconexão e arquivos maliciosos garantem que um formulário não se transforme em uma rota para DoS ou execução de conteúdo perigoso.

10 melhores cursos de programação em 2026

Descubra os melhores cursos de programação. Aprenda a escolher o curso ideal para iniciar ou avançar na carreira de desenvolvedor

POSTS RELACIONADOS

Ver todos

Seta para a direita