Upload em PHP que falha calado: limite, código de erro e pasta que não executa
Detecte quando o PHP descartou o upload antes do seu código, trate cada código de erro com uma mensagem clara e grave o arquivo fora da pasta pública, com nome gerado.
O formulário de comprovantes da Casa Pinho aceita "até 10 MB", segundo o if do código. A cliente envia um PDF de 12 MB e a tela responde "Selecione um arquivo", como se ela não tivesse escolhido nada. O PHP descartou o corpo inteiro antes de o script rodar, e o código interpretou o vazio como esquecimento. A resposta curta: antes de olhar o arquivo, detecte se o pedido estourou o limite do PHP; depois trate o código de erro de cada upload; por fim, grave com nome gerado numa pasta fora do alcance do servidor web.
Este guia foca em dois pontos que o tutorial clássico pula: o diagnóstico da falha silenciosa e o lugar onde o arquivo vai morar. Para calcular qual dos limites é o menor na sua pilha, veja o guia sobre limite de upload de verdade. Para validar o conteúdo a fundo, o de upload de arquivo em PHP.
Por que o upload some sem aviso
Um upload atravessa várias barreiras antes de chegar ao seu if. O servidor web pode recusar o corpo grande: o nginx responde 413 quando passa de client_max_body_size, e o Apache tem LimitRequestBody. Se passar, o PHP confere post_max_size, que vale para o corpo inteiro do POST. Quando esse limite é excedido, o PHP entrega $_POST e $_FILES vazios e só registra um aviso no log.
Se o corpo couber, cada arquivo ainda é comparado com upload_max_filesize. Aí o comportamento muda: o arquivo aparece em $_FILES, mas com um código de erro em vez de um arquivo temporário. Existe ainda max_file_uploads, que limita quantos arquivos vão num único pedido. O seu limite de 10 MB é o último da fila e só funciona se todos os anteriores forem maiores que ele.
São duas falhas diferentes. Na primeira, não há nada para inspecionar e o código precisa perceber pelo tamanho declarado do pedido. Na segunda, o código de erro diz exatamente o que aconteceu. O tutorial que vai direto para move_uploaded_file não trata nenhuma das duas.
Os códigos de erro que o PHP devolve
UPLOAD_ERR_OK(0): chegou inteiro.UPLOAD_ERR_INI_SIZE(1): passou deupload_max_filesize.UPLOAD_ERR_FORM_SIZE(2): passou do campo ocultoMAX_FILE_SIZEdo formulário, que o navegador controla e qualquer pessoa pode alterar.UPLOAD_ERR_PARTIAL(3): a transferência foi interrompida no meio.UPLOAD_ERR_NO_FILE(4): nenhum arquivo foi escolhido.UPLOAD_ERR_NO_TMP_DIR(6),UPLOAD_ERR_CANT_WRITE(7) eUPLOAD_ERR_EXTENSION(8): problema do servidor, não da pessoa.
Os três últimos merecem log e alerta, porque nenhum usuário consegue resolvê-los tentando de novo.
Como receber o arquivo do jeito certo
- Confira se o pedido estourou
post_max_sizecomparando o cabeçalho de tamanho com o limite configurado. - Leia o código de erro do arquivo e traduza para uma frase que a pessoa entenda.
- Aplique o seu limite de negócio sobre o tamanho real.
- Descubra o tipo pelo conteúdo, não pela extensão nem pelo tipo que o navegador declarou.
- Gere o nome do arquivo e mova para uma pasta fora da raiz pública.
- Guarde o nome original só como dado, no banco, para exibir depois.
O código completo do recebimento
<?php
declare(strict_types=1);
const LIMITE_BYTES = 10 * 1024 * 1024;
const TIPOS_ACEITOS = [
'application/pdf' => 'pdf',
'image/jpeg' => 'jpg',
'image/png' => 'png',
];
$pastaDestino = dirname(__DIR__) . '/storage/comprovantes';
function iniParaBytes(string $valor): int
{
$valor = trim($valor);
$numero = (int) $valor;
return match (strtolower(substr($valor, -1))) {
'g' => $numero * 1024 ** 3,
'm' => $numero * 1024 ** 2,
'k' => $numero * 1024,
default => $numero,
};
}
function responder(int $status, string $mensagem): never
{
http_response_code($status);
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['mensagem' => $mensagem], JSON_UNESCAPED_UNICODE);
exit;
}
$tamanhoPedido = (int) ($_SERVER['CONTENT_LENGTH'] ?? 0);
if (empty($_FILES) && $tamanhoPedido > iniParaBytes((string) ini_get('post_max_size'))) {
responder(413, 'O arquivo é maior que o permitido (até 10 MB).');
}
$arquivo = $_FILES['comprovante'] ?? null;
if (!is_array($arquivo) || is_array($arquivo['error'])) {
responder(400, 'Envie um único arquivo no campo de comprovante.');
}
switch ($arquivo['error']) {
case UPLOAD_ERR_OK:
break;
case UPLOAD_ERR_INI_SIZE:
case UPLOAD_ERR_FORM_SIZE:
responder(413, 'O arquivo é maior que o permitido (até 10 MB).');
case UPLOAD_ERR_PARTIAL:
responder(400, 'O envio foi interrompido. Tente de novo.');
case UPLOAD_ERR_NO_FILE:
responder(400, 'Selecione um arquivo.');
default:
error_log('Upload falhou no servidor, código ' . $arquivo['error']);
responder(500, 'Não conseguimos receber o arquivo agora.');
}
if ($arquivo['size'] > LIMITE_BYTES) {
responder(413, 'O arquivo é maior que o permitido (até 10 MB).');
}
$tipo = (new finfo(FILEINFO_MIME_TYPE))->file($arquivo['tmp_name']);
if (!isset(TIPOS_ACEITOS[$tipo])) {
responder(415, 'Envie PDF, JPG ou PNG.');
}
$nomeGerado = bin2hex(random_bytes(16)) . '.' . TIPOS_ACEITOS[$tipo];
$destino = $pastaDestino . '/' . $nomeGerado;
if (!move_uploaded_file($arquivo['tmp_name'], $destino)) {
error_log('move_uploaded_file falhou para ' . $destino);
responder(500, 'Não conseguimos guardar o arquivo agora.');
}
// grave $nomeGerado, $tipo, $arquivo['size'] e o nome original no banco
responder(201, 'Comprovante recebido.');
O teste de post_max_size vem primeiro porque é o único que olha para fora de $_FILES. A verificação is_array($arquivo['error']) recusa quem manda o campo como lista para confundir o código. E o switch não precisa de break depois de cada responder, porque a função termina o script e está declarada como never (PHP 8.1 ou mais novo).
A pasta onde o arquivo mora
Se a pasta de destino fica dentro da raiz pública, qualquer arquivo gravado ali pode ser pedido pela URL. Quando o servidor executa PHP naquela pasta, um foto.php enviado vira código rodando no seu servidor. Por isso o exemplo grava em storage/comprovantes, um nível acima da pasta pública. Arquivo que não tem URL não pode ser executado pela URL.
Para entregar o comprovante a quem tem direito, use um script que confere a permissão, busca o registro no banco e devolve o arquivo com Content-Type fixo, X-Content-Type-Options: nosniff e Content-Disposition: attachment. Assim o navegador baixa em vez de interpretar.
Quando a hospedagem não deixa sair da pasta pública
Em hospedagem compartilhada isso às vezes é impossível. No Apache 2.4, coloque um .htaccess na pasta de uploads que recuse qualquer arquivo com extensão de script:
<FilesMatch "(?i)\.(php[0-9]?|phtml|phar)$">
Require all denied
</FilesMatch>
Options -Indexes
Teste de verdade: grave um teste.php com um echo nessa pasta e peça pela URL. Se aparecer o texto do echo, a barreira não está ativa. A resposta esperada é 403. O nome gerado continua obrigatório mesmo com essa regra, porque ele também impede que um envio sobrescreva o arquivo de outra pessoa.
Erros comuns
- Tratar
$_FILESvazio como "não escolheu arquivo": a pessoa reenvia o mesmo PDF grande várias vezes e desiste sem saber o motivo. - Confiar no campo
MAX_FILE_SIZE: ele mora no HTML e qualquer pessoa altera; serve só como aviso antecipado. - Usar o nome original como caminho: abre espaço para sobrescrever arquivos, para nomes com
../e para extensões executáveis. - Gravar dentro da raiz pública sem barreira: um script enviado como "imagem" pode rodar no servidor.
- Checar o tipo pela extensão ou pelo
typede$_FILES: os dois vêm do cliente e mentem com facilidade. - Mostrar o caminho interno no erro: a mensagem entrega a estrutura de pastas do servidor para quem está sondando.
Checklist
- Um arquivo maior que
post_max_sizegera uma mensagem de tamanho, não "selecione um arquivo". - Cada código de erro de upload tem uma resposta própria, e os erros de servidor vão para o log.
- O limite do seu código é menor que os limites do PHP e do servidor web.
- O tipo é detectado pelo conteúdo com
finfo. - O arquivo recebe nome gerado aleatoriamente.
- A pasta de destino não é acessível pela URL, ou recusa scripts com resposta 403 testada.
- O download passa por um script que confere permissão.
Envie agora um arquivo um pouco maior que o post_max_size do seu servidor: a mensagem que aparece explica o problema ou pede para escolher um arquivo que já foi escolhido?
Referência: http://www.uolhost.com.br/faq/hospedagem/como-fazer-o-upload-de-arquivos-via-php.html