Upload cancelado, arquivo abandonado: como evitar que anexos esgotem o servidor

Janela de upload acima de um depósito de documentos e pastas, com arquivos fragmentados em primeiro plano.

O espaço consumido por uploads pode crescer sem que a lista de anexos ganhe um único item. Entre receber os primeiros bytes e vincular o arquivo a um registro, há dados que ocupam armazenamento, mas ainda não aparecem no produto. Se esse intervalo ficar sem responsável, cada abandono deixa trabalho para trás.

Uma conexão interrompida pode deixar parte de um PDF no disco. Um formulário fechado depois do envio pode deixar o arquivo inteiro sem vínculo com o chamado que seria criado. São resíduos diferentes, e nenhum depende de conteúdo malicioso. Controlar esse acúmulo exige acompanhar o ciclo de vida do upload, desde a reserva de espaço até a confirmação do anexo ou a exclusão dos bytes.

O arquivo pode existir antes de o produto reconhecer sua existência

Em um sistema de chamados, há pelo menos três recursos envolvidos: os bytes no armazenamento, um registro que descreve o upload e o vínculo com o chamado. Eles não surgem necessariamente ao mesmo tempo. Um arquivo pode ter sido gravado sem que o banco confirme o registro, ou o registro pode existir enquanto a transferência ainda está incompleta.

O primeiro passo é dar uma identidade própria ao upload antes de começar a receber o conteúdo. Em vez de usar o nome escolhido pela pessoa como caminho, a aplicação cria um identificador imprevisível e uma chave de armazenamento sob seu controle. Esse identificador acompanha o estado, o proprietário, a reserva de espaço e o prazo de abandono.

Usar uma chave nova para cada tentativa evita que a repetição da transferência sobrescreva um anexo já vinculado. O nome original fica como metadado para apresentação, sujeito a limites e tratamento adequado. Essa separação também facilita saber exatamente quais bytes podem ser removidos quando aquela tentativa falhar.

Um ciclo de vida explícito deixa as falhas visíveis

Os estados do upload podem representar essas etapas e os caminhos de descarte:

reservado -> recebendo -> recebido -> processando -> pronto -> vinculado
                 |            |             |          |
                 +------------+-------------+----------+
                              |
                     rejeitado ou abandonado
                              |
                          excluindo -> excluido

“Recebido” significa que a transferência terminou. “Pronto” significa que as validações e o processamento exigidos pelo produto terminaram. “Vinculado” significa que uma operação autorizada associou o arquivo ao chamado. Uma resposta HTTP bem-sucedida no upload não precisa pular todas essas etapas.

A separação permite estabelecer regras concretas. Um arquivo em processamento continua privado. Um anexo pronto, mas não vinculado, tem um prazo de retenção limitado. Um upload abandonado perde a possibilidade de ser finalizado. Um arquivo vinculado segue a política do chamado, e não a rotina que limpa temporários.

Nem todo projeto precisa desses nomes ou de seis estados no banco. A necessidade é conseguir distinguir quem ainda pode escrever, quem pode consumir o conteúdo e quem pode apagá-lo. Se o mesmo booleano uploaded tenta responder às três perguntas, a investigação de uma falha começa com adivinhação.

Cancelar a requisição não desfaz as gravações

O cancelamento observado pelo navegador não é uma transação de rollback no servidor. Parte do corpo pode ter chegado ao proxy, o middleware pode ter aberto um arquivo e um processamento assíncrono pode já estar em andamento. Encerrar a conexão remove um caminho de comunicação; a aplicação ainda precisa encerrar o trabalho associado quando isso for apropriado.

Também não dá para interpretar qualquer evento chamado close como abandono. Na API HTTP do Node.js, por exemplo, o significado depende do objeto observado. A documentação de IncomingMessage distingue o fechamento e o recebimento completo da mensagem, indicado por complete. Receber o corpo inteiro tampouco confirma que o anexo foi validado ou vinculado.

A rotina de interrupção deve pedir o cancelamento aos componentes que realmente suportam esse contrato, aguardar o encerramento das escritas e dos handles e só então executar a limpeza sob sua responsabilidade. Um finally é útil para recursos conhecidos pela aplicação, mas não cobre um processo morto nem corrige sozinho um middleware que deixou de informar o caminho do arquivo.

Essa ordem importa. Se a limpeza apaga um caminho enquanto uma tarefa pendente ainda consegue criar ou concluir aquele arquivo, o resíduo pode reaparecer depois da limpeza.

Em setembro de 2026, um advisory do Multer documentou uma falha específica nessa área: uploads abortados em uma janela da gravação podiam deixar arquivos órfãos no disco, sem que a aplicação recebesse uma referência para removê-los. Atualizar para a versão corrigida indicada pelo projeto trata aquela falha. Uma coleta externa não substitui o patch, e o patch não define o destino dos arquivos que foram enviados por completo e abandonados antes de salvar o formulário.

A limpeza periódica precisa disputar o arquivo com segurança

Um processo de coleta cobre o que o fluxo normal não consegue terminar: quedas do servidor, falhas de armazenamento, tarefas interrompidas e formulários abandonados. Apagar tudo que tem uma data antiga, porém, pode atingir um upload lento ou um processamento válido.

Uma forma de coordenar a coleta é considerar o estado e o vencimento de uma concessão de trabalho, ou lease. Enquanto recebe ou processa o arquivo, o responsável mantém essa concessão válida. Quando ela expira, um coletor tenta assumir a exclusão por uma mudança condicional no banco. A finalização do anexo precisa respeitar a mesma regra de concorrência.

Um trecho ilustrativo em PostgreSQL poderia assumir a exclusão de um upload já classificado como abandonado ou rejeitado:

UPDATE uploads
SET state = 'excluindo',
    cleanup_token = :cleanup_token
WHERE id = :upload_id
  AND state IN ('abandonado', 'rejeitado')
  AND lease_until < CURRENT_TIMESTAMP
RETURNING storage_key;

Os parâmetros são valores vinculados pelo driver. O cleanup_token identifica a tentativa de coleta. Se nenhuma linha voltar, esse coletor não adquiriu a responsabilidade pela exclusão. O comando ilustra a disputa pelo registro; ele não interrompe um gravador que já estava escrevendo no filesystem ou em um serviço externo.

Por isso, o protocolo inteiro precisa impedir novas finalizações depois da transição e lidar com operações que já estavam em voo. Num sistema com gravadores próprios, eles devem verificar a posse do trabalho e encerrar a escrita antes da remoção definitiva. Em uploads diretos a outro serviço, pode ser necessário esperar a janela de escrita terminar e repetir a verificação de resíduos. Marcar uma linha no banco não revoga automaticamente uma autorização externa.

Depois de assumir a coleta, o worker remove o objeto conhecido e registra o término. Se cair entre essas duas etapas, outra tentativa retoma o estado excluindo, com regras próprias de posse e recuperação. “Arquivo já ausente” pode ser sucesso; erro de permissão ou indisponibilidade precisa continuar pendente. Limpar o registro primeiro e esquecer os bytes perde justamente a referência necessária para tentar novamente.

Também vale reconciliar periodicamente o inventário do armazenamento com os registros da aplicação. Arquivos sem registro exigem uma margem de idade e um espaço de nomes reconhecido como temporário. A coleta deve operar nesse espaço controlado, sem transformar um nome enviado pelo usuário em caminho de exclusão e sem percorrer arbitrariamente o disco do servidor.

Reservar espaço evita que o limite exista apenas no formulário

Limitar cada arquivo ajuda, mas não limita a soma. Cem uploads permitidos individualmente podem ocupar todo o espaço disponível. Antes de receber o corpo, o produto pode reservar uma parcela da cota do usuário ou da organização, limitar quantas transferências estão em andamento e conferir a capacidade global do armazenamento temporário.

Na transferência, é necessário contar os bytes efetivamente recebidos. O tamanho declarado pelo cliente não deve ser a única defesa. O proxy e o parser também precisam de limites compatíveis: se o proxy grava o corpo em disco antes de chamar a aplicação, a autenticação dentro do handler não protege sozinha aquele espaço temporário.

Para o sistema de chamados, uma reserva inclui o arquivo de entrada e uma margem para os derivados previstos. Se o processamento gera miniaturas ou descompacta um arquivo, os bytes finais podem superar o tamanho recebido. A aplicação deve impor limites de tempo, memória e saída para esse trabalho, liberar reservas no término e recuperar reservas vencidas após falhas.

A OWASP reúne controles de upload como limites de tamanho, nomes controlados pela aplicação, validação de tipos e armazenamento fora da área pública. Esses controles continuam necessários. O ciclo de vida acrescenta uma pergunta operacional: o que acontece com os recursos alocados quando o fluxo não chega ao final?

Upload direto para object storage muda o lugar do problema

Enviar o arquivo diretamente do navegador para um serviço de objetos reduz o tráfego e o armazenamento temporário da API. Os bytes passam a ocupar outro lugar, mas ainda podem ficar sem vínculo com o produto. A aplicação deve continuar criando a intenção do upload, limitando sua validade e conferindo o objeto antes de aceitar a finalização.

Na confirmação, o backend verifica o proprietário, a chave esperada, o tamanho observado e os controles de integridade disponíveis no contrato adotado. Um cliente que informa “terminei” não decide sozinho que um objeto está pronto para ser servido. A autorização de acesso ao chamado também continua sendo do servidor.

Como exemplo específico, URLs pré-assinadas do Amazon S3 podem ser usadas mais de uma vez até expirar e podem sobrescrever um objeto com a mesma chave. Não são, por definição, autorizações de uso único. O fluxo precisa considerar essa janela, inclusive uma transferência iniciada antes do vencimento que ainda esteja em andamento. Outros serviços devem ser avaliados conforme seu próprio contrato.

Outro detalhe do S3: a ação de lifecycle AbortIncompleteMultipartUpload limpa partes de transferências multipart não concluídas. Ela não remove objetos completos que o usuário enviou e abandonou antes de salvar o chamado. São classes de resíduo diferentes e precisam de políticas correspondentes.

Finalizar o anexo é uma operação de negócio

Quando a pessoa envia o formulário, o servidor confere se cada upload pertence a ela, continua disponível e pode ser associado àquele chamado. A criação do vínculo e a mudança de estado precisam ser coordenadas para que uma coleta concorrente não remova um arquivo aceito pelo produto.

Uma confirmação repetida após timeout também precisa de um resultado estável. Ela não deve criar vários anexos nem cobrar espaço de novo para o mesmo vínculo. A discussão sobre chaves de idempotência em APIs ajuda a desenhar essa confirmação, enquanto a posse do arquivo e a autorização do chamado continuam sendo verificadas em cada operação.

Quando a autenticação usa cookie, iniciar, finalizar ou excluir uploads também exige avaliar a proteção CSRF. O arquivo temporário deve permanecer privado e separado da área de arquivos aprovados. Assim, “foi recebido” não vira permissão implícita para exibir conteúdo que ainda não passou pelo processo previsto.

O número útil é o que ficou para trás

Uma taxa alta de sucesso HTTP pode conviver com um diretório temporário em crescimento. Vale acompanhar bytes e quantidade de objetos por estado, idade do upload não vinculado mais antigo, reservas vencidas, falhas de exclusão e arquivos do armazenamento que não possuem registro correspondente.

Esses sinais levam a ações diferentes. Muitos uploads interrompidos pedem investigar clientes, limites e transporte. Arquivos prontos sem vínculo podem revelar abandono do formulário. Itens presos em excluindo apontam para permissões ou falhas do coletor. Um alerta de ocupação global continua necessário, mas essa separação ajuda a chegar à causa antes de o disco se tornar a primeira evidência do problema.

Fontes