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 busboyParser 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 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 | rejectedRotas 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.



