Programação Por Equipe DevBras

Capa do artigo: Limite de upload: descubra qual camada barra o arquivo antes do seu código

Limite de upload: descubra qual camada barra o arquivo antes do seu código

Identifique pelo sintoma quem cortou o envio, Nginx, Apache, post_max_size ou upload_max_filesize, alinhe os tetos com a sua regra e devolva uma mensagem clara em vez de erro vazio.

O site da imobiliária diz "até 5 fotos de 8 MB cada". O corretor escolhe cinco fotos de 6 MB, clica em enviar e, depois de uma barra de progresso inteira, recebe uma página branca do Nginx com "413 Request Entity Too Large". Em outra máquina, com outra hospedagem, o mesmo envio volta para o formulário vazio, sem mensagem nenhuma. A resposta curta: o limite que vale é o menor de todas as camadas, e cada uma falha de um jeito. Descubra pelo sintoma qual cortou, deixe todos os tetos técnicos acima da sua regra de negócio e faça a recusa ser a sua, com mensagem clara.

Este guia é o diagnóstico camada por camada. A parte de onde gravar o arquivo e como validar o conteúdo está em upload em PHP: o limite não é só o do seu if e em aceitar o arquivo sem aceitar o que vem dentro.

Por que o seu if quase nunca é quem recusa

O arquivo atravessa várias camadas antes de o seu código rodar, e cada uma tem um teto próprio:

  • CDN, WAF ou balanceador na frente do site, com limite de corpo por plano.
  • Proxy ou ingress, como o ingress-nginx no Kubernetes, configurado pela anotação nginx.ingress.kubernetes.io/proxy-body-size.
  • Servidor web: no Nginx, client_max_body_size, cujo padrão é 1 MB; no Apache, LimitRequestBody.
  • PHP: post_max_size, o teto da requisição inteira, com padrão de 8 MB; upload_max_filesize, o teto de cada arquivo, com padrão de 2 MB; e max_file_uploads, a quantidade de arquivos por envio.
  • A sua regra: os 8 MB por foto que a interface promete.

O pedido só chega ao seu código se passar por todas. Os padrões explicam o caso clássico de "a aplicação aceita 20 MB e ninguém passa de 2 MB": ninguém mexeu no upload_max_filesize. E repare que os limites são de coisas diferentes. Cinco fotos de 6 MB cabem na regra de 8 MB por foto, mas somam 30 MB numa requisição só, e é o teto da requisição que estoura.

Como saber quem barrou, pelo sintoma

  • Página de erro 413 com a assinatura do servidor web, sem o visual do seu site: foi o Nginx, o Apache ou um proxy antes deles. O PHP nem rodou.
  • Página de erro da CDN, com o nome do serviço: foi a borda.
  • $_POST e $_FILES vazios, com CONTENT_LENGTH preenchido: estourou o post_max_size. O PHP descarta o corpo inteiro e registra um aviso no log de erros dizendo que o Content-Length passou do limite.
  • $_FILES com error igual a UPLOAD_ERR_INI_SIZE: aquele arquivo passou do upload_max_filesize; os outros campos chegaram.
  • Só os primeiros arquivos chegam: bateu no max_file_uploads.
  • A sua mensagem: tudo certo, quem recusou foi a regra de negócio.

Como medir e alinhar os tetos

  1. Calcule o pior caso da sua regra. Cinco fotos de 8 MB são 40 MB, mais os campos de texto e a sobra do formato multipart.
  2. Defina os tetos técnicos com folga acima disso: 10 MB por arquivo e 48 MB por requisição, por exemplo.
  3. Leia o que o PHP do site realmente usa, pela web, não pelo terminal. O PHP de linha de comando costuma ler outro php.ini.
  4. Ajuste o PHP, o servidor web e o proxy, de dentro para fora.
  5. Teste com arquivos logo abaixo e logo acima de cada limite e confira o sintoma de cada um.
  6. Trate no código os casos que chegam ao PHP, para devolver mensagem clara.
  7. Confira o tamanho no navegador antes de enviar, para poupar a espera de quem escolheu um arquivo grande demais.

Lendo os limites reais

<?php
// limites.php: apague depois de conferir
header('Content-Type: text/plain; charset=utf-8');
foreach (['upload_max_filesize', 'post_max_size', 'max_file_uploads', 'max_input_time'] as $chave) {
    echo $chave . ' = ' . ini_get($chave) . PHP_EOL;
}
echo 'php.ini = ' . php_ini_loaded_file() . PHP_EOL;

Acessado pelo navegador, ele mostra os valores do PHP-FPM ou do módulo do Apache que atende o site, e qual arquivo de configuração foi carregado. Não deixe esse arquivo publicado.

Ajustando cada camada

; php.ini, ou .user.ini na pasta do site quando o PHP roda como FPM ou CGI
upload_max_filesize = 10M
post_max_size = 48M
max_file_uploads = 20
# Nginx, no bloco http, server ou location do upload
client_max_body_size 48m;
# Apache, em bytes
LimitRequestBody 50331648

No PHP, M significa 1.048.576 bytes, então 48M são 50.331.648 bytes, o mesmo valor do Apache. Esses dois limites do PHP não podem ser mudados com ini_set() dentro do script, porque o corpo da requisição já foi lido quando o seu código roda. O .user.ini vale para PHP como FPM ou CGI; com o módulo do Apache, o caminho é php_value no .htaccess. Depois de mudar o php.ini principal, reinicie o PHP-FPM; depois do Nginx, rode nginx -t e recarregue. Em hospedagem compartilhada, algumas dessas camadas não são suas, e o número da hospedagem é o teto.

Testando com arquivos de tamanho conhecido

head -c 9M /dev/urandom > foto-9mb.bin
head -c 11M /dev/urandom > foto-11mb.bin

curl -i -F "fotos[]=@foto-9mb.bin" https://homolog.imobiliaria.exemplo/anuncios/123/fotos
curl -i -F "fotos[]=@foto-11mb.bin" https://homolog.imobiliaria.exemplo/anuncios/123/fotos

O primeiro deve chegar ao seu código e ser recusado pela regra de 8 MB, com a sua mensagem. O segundo deve bater no upload_max_filesize e também receber uma mensagem sua, porque o PHP ainda roda nesse caso. Repita com seis arquivos de 9 MB para ver o teto da requisição, e anote o que cada camada devolveu.

Tratando no código o que chega ao PHP

<?php
const LIMITE_FOTO = 8 * 1024 * 1024;

if ($_SERVER['REQUEST_METHOD'] === 'POST' && empty($_POST) && empty($_FILES)
    && (int) ($_SERVER['CONTENT_LENGTH'] ?? 0) > 0) {
    http_response_code(413);
    exit('O envio passou do tamanho total permitido. Envie menos fotos de cada vez.');
}

$erros = [];
foreach ($_FILES['fotos']['error'] ?? [] as $i => $erro) {
    $nome = $_FILES['fotos']['name'][$i];
    if ($erro === UPLOAD_ERR_INI_SIZE || $erro === UPLOAD_ERR_FORM_SIZE) {
        $erros[] = $nome . ': a foto passa de 8 MB.';
    } elseif ($erro === UPLOAD_ERR_PARTIAL) {
        $erros[] = $nome . ': o envio foi interrompido. Tente de novo.';
    } elseif ($erro === UPLOAD_ERR_OK && $_FILES['fotos']['size'][$i] > LIMITE_FOTO) {
        $erros[] = $nome . ': a foto passa de 8 MB.';
    }
}

O primeiro bloco detecta o estouro do post_max_size, que de outra forma pareceria um formulário enviado em branco. O laço trata cada foto pelo código de erro que o PHP preencheu, e só então aplica a regra de 8 MB. Para a pessoa, as três situações dizem a mesma coisa útil: qual foto e qual é o limite.

Avisando antes de enviar

const campo = document.getElementById('fotos');
const LIMITE = 8 * 1024 * 1024;

campo.addEventListener('change', function () {
  const grandes = Array.from(campo.files).filter(function (f) { return f.size > LIMITE; });
  campo.setCustomValidity(grandes.length
    ? 'Estas fotos passam de 8 MB: ' + grandes.map(function (f) { return f.name; }).join(', ')
    : '');
  campo.reportValidity();
});

Isso só poupa espera; o servidor continua conferindo. Para a barra de progresso que acompanha o envio, veja barra de progresso do upload.

Erros comuns

  • Aumentar só o upload_max_filesize. O arquivo passa do teto por arquivo e esbarra no post_max_size, que agora descarta o formulário inteiro sem aviso.
  • Conferir o limite com php -i no terminal. O PHP de linha de comando lê outro arquivo, e o número que você vê não é o do site.
  • Esquecer o Nginx ou o proxy. O PHP aceita 48 MB, mas o padrão de 1 MB do client_max_body_size corta antes.
  • Usar ini_set() para subir o limite. Não tem efeito, porque o corpo já foi processado quando a linha roda.
  • Regra de negócio acima do teto técnico. A interface promete 10 MB, a camada corta em 8, e a pessoa vê uma página de erro sem explicação.
  • Esquecer que vários arquivos somam. Cada foto cabe, o envio inteiro não.

Checklist

  • Você sabe o pior caso da sua regra: quantidade vezes tamanho, mais os campos.
  • upload_max_filesize e post_max_size lidos pela web estão acima desse pior caso.
  • client_max_body_size, LimitRequestBody e o proxy estão alinhados com o post_max_size.
  • O estouro do post_max_size devolve uma mensagem, não um formulário em branco.
  • Cada código de erro do $_FILES vira uma frase com o nome do arquivo.
  • O navegador avisa o arquivo grande antes de enviar.
  • Você testou abaixo e acima de cada limite e anotou o resultado.
  • O número escrito na interface é o número que de fato passa.

Envie hoje um arquivo de teste logo acima do limite que a sua interface promete. Quem respondeu: a sua mensagem, uma página 413 sem estilo ou um formulário vazio? E dessas camadas, qual você consegue ajustar e qual pertence à hospedagem?

Avalie este artigo

Referência: http://manual.webhost.com.br/content/limite-de-upload