Dominar uploads de arquivos: guia completo para desenvolvedores
Um upload de arquivo pode chegar ao servidor e ainda falhar antes que qualquer dado seja armazenado. Este guia apresenta manipuladores locais de upload de imagens para Node.js, Flask e PHP, uma integração Rails autenticada e um fluxo de trabalho no Postman para distinguir falhas na requisição, na validação e no armazenamento.
Introdução: o que é um upload de arquivo?
Um navegador envia os bytes do arquivo selecionado a um endpoint receptor. Com multipart/form-data,
cada arquivo ou campo de texto é uma parte separada com um nome de campo. O receptor interpreta essas
partes, aplica sua política de aceitação e grava os bytes aceitos no armazenamento. Um nome de arquivo
ou tipo MIME fornecido pelo cliente é um rótulo, não uma prova do conteúdo desses bytes.
Configurar um recurso básico de upload de arquivos
Escolha um manipulador e execute apenas um por vez em 127.0.0.1:3000. Este formulário HTML envia um arquivo com
o nome de campo myfile:
<form action="http://127.0.0.1:3000/upload" method="post" enctype="multipart/form-data">
<label for="myfile">Image</label>
<input id="myfile" type="file" name="myfile" />
<button type="submit">Upload File</button>
</form>
O atributo enctype="multipart/form-data" é essencial porque instrui o navegador a enviar os dados do
arquivo junto com os campos padrão do formulário.
Os programas Node, Flask e PHP abaixo são demonstrações locais de manipuladores de upload. Vincule-os à interface de loopback e mantenha o armazenamento fora de qualquer raiz de arquivos estáticos ou documentos. Eles não implementam login: antes da implantação, adicione autenticação, autorização, proteção contra CSRF, cotas por usuário e limites de requisições na camada de acesso a eles. O trecho Rails se integra a uma aplicação autenticada existente.
Use Bash, com Node.js e Corepack instalados separadamente; consulte o guia de instalação do Yarn. As versões testadas são Node.js 24.21.0 e Yarn 4.18.0; são versões fixadas para reprodução, não versões mínimas intrínsecas. Trabalhe fora de um projeto JavaScript existente para que suas configurações de gerenciador de pacotes não alterem a instalação. Cole este bloco no shell; seu subshell preserva o diretório de trabalho e as opções do shell em caso de falha:
(
for tool in node corepack; do
command -v "$tool" >/dev/null || { printf 'Missing %s\n' "$tool" >&2; exit 1; }
done
[ -z "${YARN_RC_FILENAME:-}" ] || { printf 'Use the default Yarn configuration filename\n' >&2; exit 1; }
node --input-type=module -e '
import { existsSync } from "node:fs";
import { dirname, join } from "node:path";
let directory = process.cwd();
for (;;) {
if (["package.json", "yarn.lock", ".yarnrc.yml", ".pnp.cjs"].some(name => existsSync(join(directory, name)))) {
throw new Error("Choose a directory outside an existing JavaScript project");
}
const parent = dirname(directory);
if (parent === directory) break;
directory = parent;
}
' &&
mkdir upload-node && cd upload-node &&
printf '%s\n' '{"private":true,"type":"module","packageManager":"yarn@4.18.0"}' > package.json &&
printf 'nodeLinker: node-modules\nenableGlobalCache: false\n' > .yarnrc.yml &&
corepack yarn add express@5.2.1 express-fileupload@1.5.2 file-type@22.1.0 &&
corepack yarn node --input-type=module -e "await import('express'); await import('express-fileupload'); await import('file-type')"
)
O bloco cria upload-node, mas mantém o shell que o chamou no diretório original. Se a instalação
falhar, mantenha esse diretório e seus arquivos. Depois de corrigir a conectividade ou o problema de
instalação relatado, tente novamente com a lista explícita de dependências; um yarn install sem argumentos
pode ter sucesso com um manifesto vazio após uma falha inicial na resolução de dependências:
(
cd upload-node &&
corepack yarn add express@5.2.1 express-fileupload@1.5.2 file-type@22.1.0 &&
corepack yarn node --input-type=module -e "await import('express'); await import('express-fileupload'); await import('file-type')"
)
Salve como upload-node/upload.ts. O parser mantém os dados dos arquivos na memória, com até dois buffers de arquivo
por requisição. O manipulador exige um tamanho declarado para o corpo, permite duas respostas HTTP
ativas e gera os caminhos de armazenamento no servidor. Seu contador HTTP libera a contagem quando a
resposta é encerrada; ele não limita gravações assíncronas que ainda estejam terminando depois que um
cliente se desconecta.
import { randomUUID } from 'node:crypto'
import { mkdir, rm, writeFile } from 'node:fs/promises'
import { join } from 'node:path'
import express from 'express'
import fileUpload from 'express-fileupload'
import { fileTypeFromBuffer } from 'file-type'
const app = express()
const maxBytes = 5 * 1024 * 1024
const root = join(process.cwd(), 'private-uploads')
await mkdir(root, { recursive: true, mode: 0o700 })
let active = 0
const parse = fileUpload({
// Busboy signals its limit at the threshold; the extra byte keeps maxBytes inclusive.
limits: { fileSize: maxBytes + 1, files: 2, fields: 0, parts: 3 },
abortOnLimit: true,
useTempFiles: false,
uploadTimeout: 10_000,
})
app.post('/upload', (req, res, next) => {
const length = req.get('content-length') ?? ''
if (!/^[1-9][0-9]*$/.test(length)) return res.status(411).send('Content length required')
if (Number(length) > 6 * 1024 * 1024) return res.status(413).send('Request too large')
if (active >= 2) return res.status(503).send('Busy')
active += 1
res.once('close', () => { active -= 1 })
next()
}, parse, async (req, res) => {
const files = req.files
const file = files?.myfile
if (!files || Object.keys(files).length !== 1 || !file || Array.isArray(file)) {
return res.status(400).send('Send exactly one myfile')
}
if (file.truncated || file.size < 1 || file.size > maxBytes) {
return res.status(413).send('Invalid file size')
}
let type
try {
type = await fileTypeFromBuffer(file.data.subarray(0, 4100))
} catch {
return res.status(400).send('Unrecognized image')
}
const extensions = new Map([['image/jpeg', 'jpg'], ['image/png', 'png'], ['image/gif', 'gif']])
const extension = extensions.get(type?.mime)
if (!extension) return res.status(400).send('Unrecognized image')
const directory = join(root, randomUUID())
let created = false
try {
await mkdir(directory, { mode: 0o700 })
created = true
await writeFile(join(directory, 'image.' + extension), file.data, { flag: 'wx', mode: 0o600 })
if (res.destroyed) {
await rm(directory, { recursive: true, force: true })
return
}
res.status(201).send('File uploaded successfully')
} catch {
if (created) await rm(directory, { recursive: true, force: true })
throw new Error('Storage failed')
}
})
app.use((_error, _req, res, _next) => {
if (!res.headersSent) res.status(500).send('Upload failed')
})
const server = app.listen(Number(process.env.PORT ?? 3000), '127.0.0.1', (error) => {
if (error) {
console.error('Upload server could not start')
process.exitCode = 1
}
})
server.requestTimeout = 15_000
server.headersTimeout = 10_000
Inicie o servidor no mesmo shell em que executou a configuração:
(
cd upload-node && corepack yarn node upload.ts
)
Deixe esse terminal aberto e pare o servidor com Ctrl+C quando terminar. Salve o formulário como form.html e abra-o no navegador, ou use as
etapas do Postman abaixo. Cada requisição aceita retorna 201 e cria um novo diretório privado contendo
image.jpg, image.png ou image.gif; o nome de arquivo enviado nunca é usado como caminho de armazenamento.
express-fileupload fornece o parser multipart;
não é inerentemente mais seguro que outro parser mantido.
file-type identifica assinaturas, não a segurança do arquivo.
Um PNG com bytes de script anexados ainda pode ser identificado como PNG. O armazenamento privado e
as extensões geradas impedem que o manipulador crie um arquivo executável pela web com um nome
fornecido pelo cliente; a entrega pública segura de imagens exige uma política separada de
decodificação/recodificação e varredura de segurança.
Lidar com uploads de arquivos em diferentes frameworks
Python (Flask)
O ambiente de execução Python testado é o 3.12.3, com Flask 3.1.3 e Pillow 12.3.0. O módulo venv do Python e
o pip precisam estar instalados. Em um novo diretório de trabalho, use um projeto separado e evite
ativar seu venv no shell que o chamou:
(
command -v python3 >/dev/null || { printf 'Missing python3\n' >&2; exit 1; }
mkdir upload-flask && cd upload-flask &&
python3 -m venv .venv &&
.venv/bin/python -m pip install Flask==3.1.3 Pillow==12.3.0
)
Salve como upload-flask/app.py. O manipulador completo cria armazenamento privado, verifica a imagem com um parser
real e usa apenas uma extensão escolhida pelo servidor. O limite da requisição inclui a sobrecarga
multipart; o limite do arquivo é verificado separadamente.
from io import BytesIO
from pathlib import Path
import os
import uuid
import warnings
from flask import Flask, abort, request
from PIL import Image, UnidentifiedImageError
app = Flask(__name__)
app.config['MAX_CONTENT_LENGTH'] = 6 * 1024 * 1024
# Keep Flask's default form-memory limit; Werkzeug also applies it to parser buffers.
app.config['MAX_FORM_PARTS'] = 2
root = Path.cwd() / 'private-uploads'
root.mkdir(mode=0o700, exist_ok=True)
Image.MAX_IMAGE_PIXELS = 20_000_000
extensions = {'JPEG': 'jpg', 'PNG': 'png', 'GIF': 'gif'}
@app.post('/upload')
def upload():
if set(request.files) != {'myfile'} or len(request.files.getlist('myfile')) != 1:
abort(400)
data = request.files['myfile'].stream.read(5 * 1024 * 1024 + 1)
if not data or len(data) > 5 * 1024 * 1024:
abort(413)
try:
with warnings.catch_warnings():
warnings.simplefilter('error', Image.DecompressionBombWarning)
with Image.open(BytesIO(data), formats=['JPEG', 'PNG', 'GIF']) as image:
extension = extensions.get(image.format)
image.verify()
except (UnidentifiedImageError, OSError, SyntaxError, ValueError,
Image.DecompressionBombError, Image.DecompressionBombWarning):
abort(400)
if extension is None:
abort(400)
destination = root / (uuid.uuid4().hex + '.' + extension)
created = False
try:
descriptor = os.open(destination, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
created = True
with os.fdopen(descriptor, 'wb') as output:
output.write(data)
except OSError:
if created:
destination.unlink(missing_ok=True)
abort(500)
return 'File uploaded successfully.', 201
@app.errorhandler(500)
def storage_error(_error):
return 'Upload failed.', 500
if __name__ == '__main__':
app.run(host='127.0.0.1', port=int(os.environ.get('PORT', '3000')), debug=False)
Inicie-o no shell original e pare-o com Ctrl+C:
(
cd upload-flask && .venv/bin/python app.py
)
Se o pip falhou depois de criar o venv, mantenha-o e tente novamente no shell original:
(
cd upload-flask && .venv/bin/python -m pip install Flask==3.1.3 Pillow==12.3.0
)
O servidor integrado é destinado ao desenvolvimento local. Use o mesmo formulário e o mesmo campo myfile do Node.
A verificação do Pillow
não recodifica a imagem nem remove conteúdo anexado. Armazene os originais de forma privada; isole o
processamento de imagens e imponha limites de CPU/memória antes de aceitar tráfego público não
confiável.
PHP
Este exemplo foi testado com PHP 8.4.26 e sua extensão FileInfo. Crie os diretórios primeiro:
(
command -v php >/dev/null || { printf 'Missing php\n' >&2; exit 1; }
php -r 'exit(extension_loaded("fileinfo") ? 0 : 1);' &&
mkdir upload-php && cd upload-php &&
mkdir public && mkdir -m 700 private-uploads
)
Salve o manipulador como upload-php/public/upload.php. Inicie-o no shell original e pare-o com Ctrl+C:
(
cd upload-php &&
php -d display_errors=0 -d upload_max_filesize=5M -d post_max_size=6M -d max_file_uploads=2 -S 127.0.0.1:3000 -t public
)
Defina a ação do formulário ou a URL do Postman como
http://127.0.0.1:3000/upload.php. O PHP deve ser o proprietário do diretório privado e ter permissão para gravar nele.
O contrato de upload do PHP inclui um
código de erro de upload e um caminho temporário no servidor. Verifique-os antes de inspecionar o
conteúdo. Nunca preserve a extensão enviada: um arquivo chamado picture.php pode conter bytes de imagem.
<?php
declare(strict_types=1);
header('Content-Type: text/plain; charset=utf-8');
header('Cache-Control: no-store');
function rejectUpload(int $status): never {
http_response_code($status);
exit('Upload rejected.');
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Allow: POST');
rejectUpload(405);
}
if ((int) ($_SERVER['CONTENT_LENGTH'] ?? 0) > 6 * 1024 * 1024) {
rejectUpload(413);
}
$file = $_FILES['myfile'] ?? null;
if (count($_FILES) !== 1 || !is_array($file) ||
!isset($file['error']) || !is_int($file['error'])) {
rejectUpload(400);
}
if ($file['error'] === UPLOAD_ERR_INI_SIZE || $file['error'] === UPLOAD_ERR_FORM_SIZE) {
rejectUpload(413);
}
if ($file['error'] !== UPLOAD_ERR_OK || !is_string($file['tmp_name'] ?? null) ||
!is_uploaded_file($file['tmp_name'])) {
rejectUpload(400);
}
$directory = null;
try {
$size = filesize($file['tmp_name']);
if ($size === false || $size < 1 || $size > 5 * 1024 * 1024) {
rejectUpload(413);
}
$mime = (new finfo(FILEINFO_MIME_TYPE))->file($file['tmp_name']);
$extensions = ['image/jpeg' => 'jpg', 'image/png' => 'png', 'image/gif' => 'gif'];
if (!is_string($mime) || !isset($extensions[$mime])) {
rejectUpload(400);
}
$root = dirname(__DIR__) . '/private-uploads';
if (!is_dir($root) || !is_writable($root)) {
throw new RuntimeException('Storage unavailable');
}
$candidate = $root . '/' . bin2hex(random_bytes(16));
if (!mkdir($candidate, 0700)) {
throw new RuntimeException('Storage unavailable');
}
$directory = $candidate;
$destination = $directory . '/image.' . $extensions[$mime];
if (!move_uploaded_file($file['tmp_name'], $destination) || !chmod($destination, 0600)) {
throw new RuntimeException('Storage unavailable');
}
http_response_code(201);
echo 'File uploaded successfully.';
} catch (Throwable $error) {
if ($directory !== null) {
if (isset($destination) && is_file($destination)) {
unlink($destination);
}
rmdir($directory);
}
error_log('Upload storage failed');
http_response_code(500);
echo 'Upload failed.';
}
Desative display_errors em uma implantação HTTP para que os avisos do sistema de arquivos do PHP não exponham caminhos.
O FileInfo identifica o tipo pelo conteúdo; não é uma varredura de malware nem um decodificador
completo de imagens. O PHP remove seus arquivos temporários de upload quando a requisição termina; o
manipulador move apenas os arquivos aceitos para o armazenamento privado.
O subdiretório privado aleatório impede
a sobrescrita por move_uploaded_file()
de outros uploads aceitos.
Ruby on Rails
Os validadores attached, content_type e size vêm de
active_storage_validations, não do
próprio Rails. Este exemplo usa Rails 8.1.3.1, o ambiente de execução Ruby 3.3.10 testado e a versão
4.1.1 dessa gem. Adicione-a ao Gemfile da sua aplicação existente e execute bundle install:
gem 'active_storage_validations', '4.1.1'
Mantenha as dependências resolvidas da sua aplicação em Gemfile.lock.
Use uma aplicação autenticada existente com Active Storage instalado. Configure um serviço Disk
privado ou armazenamento de objetos privado seguindo o
guia do Active Storage.
Para um serviço Disk local, coloque sua raiz fora de public e atribua a esse diretório o modo 0700.
A opção contra falsificação de conteúdo abaixo também precisa do executável UNIX file. As versões testadas
desse utilitário foram file 5.46 e 5.48; sua classificação de PNG difere, como descrito na tabela de aceitação abaixo.
Este host usou Rack 3.2.7. Mantenha o utilitário instalado separadamente das gems Ruby.
# app/models/user.rb
class User < ApplicationRecord
has_one_attached :myfile
validates :myfile,
content_type: { in: ['image/png', 'image/jpeg', 'image/gif'], spoofing_protection: true },
size: { between: 1..5.megabytes }
end
Em uma aplicação cuja camada de autenticação já fornece current_user, use este
controlador. Não selecione um usuário a partir de params[:user_id]. Ele aceita apenas um novo upload
multipart, não um ID assinado de blob do Active Storage fornecido pelo cliente.
# app/controllers/uploads_controller.rb
class UploadsController < ApplicationController
before_action :require_upload_user
protect_from_forgery with: :exception
def new
render :new
end
def create
file = params.require(:user).permit(:myfile)[:myfile]
unless file.is_a?(ActionDispatch::Http::UploadedFile)
return render plain: 'Send a file.', status: :bad_request
end
user = current_user
if user.update(myfile: file)
render plain: 'File uploaded successfully.', status: :created
else
reasons = user.errors.details[:myfile].map { |detail| detail[:error].to_s }.uniq
Rails.logger.warn('Upload validation rejected: ' + reasons.join(', '))
render plain: 'File rejected.', status: :unprocessable_entity
end
end
private
def require_upload_user
head :unauthorized unless current_user
end
end
Adicione estas rotas a config/routes.rb:
get '/upload', to: 'uploads#new'
post '/upload', to: 'uploads#create'
Salve esta view como app/views/uploads/new.html.erb. O Rails gera o token CSRF e o nome de campo aninhado
user[myfile] esperado pelo controlador:
<%= form_with scope: :user, url: '/upload', multipart: true do |form| %>
<%= form.label :myfile, 'Image' %>
<%= form.file_field :myfile, accept: 'image/png,image/jpeg,image/gif', required: true %>
<%= form.submit 'Upload file' %>
<% end %>
Inicie a aplicação existente no diretório do projeto:
bin/rails server -b 127.0.0.1 -p 3000
Entre pelo login normal da aplicação e acesse /upload. Pare o servidor local com Ctrl+C.
current_user deve retornar o
User autenticado; o recurso mantém os anexos opcionais para que a criação de um usuário ou a edição de seu nome continue funcionando.
Mantenha o serviço de armazenamento privado e configure controladores de download autenticados. As URLs assinadas padrão do Active Storage não fornecem autorização por usuário. Imponha um limite de corpo de requisição no servidor web antes que o Rails interprete os dados multipart. A validação do modelo ocorre depois da análise e não pode fornecer esse limite de recursos de rede. Estes exemplos não habilitam uploads diretos; blobs não utilizados de outros fluxos precisam de uma política de retenção separada.
Problemas comuns de upload de arquivos e soluções
Por que meu arquivo não é enviado por upload?
Comece pela resposta e verifique qual etapa recusou a requisição. Todos esses manipuladores locais exigem um arquivo não vazio de no máximo 5 MiB (5.242.880 bytes), incluindo exatamente 5 MiB. Node, Flask e PHP também limitam o corpo declarado da requisição a 6 MiB, incluindo a sobrecarga multipart. O Rails precisa que esse limite de corpo seja imposto pelo servidor web existente antes da validação do modelo.
| Sintoma | O que verificar nestes exemplos |
|---|---|
| 404 ou 405 | Confirme a URL e o método POST. O PHP recebe /upload.php; os outros exemplos recebem /upload. |
| 400 | Verifique a codificação multipart, o campo esperado e a política de reconhecimento de conteúdo abaixo. O Flask também retorna 400 ao exceder seu limite de pixels ou quando a verificação da imagem falha. O parser do Node pode não deixar nenhum arquivo após um tempo limite de inatividade no upload, produzindo o mesmo 400 de um campo ausente. |
| 411 no Node | Envie um tamanho de corpo conhecido. Requisições transmitidas em chunks são recusadas por esta demonstração. |
| 413 | Verifique os limites de bytes do arquivo e da requisição. Node, Flask e PHP também usam 413 para um arquivo vazio. O parser do Flask pode retornar 413 por excesso de partes ou por seu limite de memória de formulário. |
| 401 no Rails | Entre pela aplicação que hospeda o recurso. Um upload não pode escolher um usuário por ID. |
| 422 no Rails | Forneça o token CSRF da sessão e o campo de arquivo aninhado user[myfile]. Uma requisição com CSRF válido ainda pode falhar na validação de conteúdo ou tamanho, inclusive por um arquivo vazio. |
| 500 | Verifique as permissões de armazenamento. O host Rails testado também retornou 500 nos limites de arquivos/partes do Rack antes que o manipulador fosse executado. |
| 503 no Node | Duas respostas HTTP já estão ativas. Aguarde sua conclusão e tente novamente. |
Um PNG pequeno pode exceder um limite de pixels mesmo ficando abaixo do limite de bytes. O Flask
recusa imagens acima de 20.000.000 pixels; imagens com exatamente essa quantidade de pixels passam na
verificação de dimensões. Sua chamada verify() verifica a estrutura do arquivo sem decodificar
completamente os pixels. A identificação por assinatura do Node, o FileInfo do PHP e a verificação de
falsificação de MIME do Rails também não comprovam que toda imagem pode ser decodificada, que os
metadados foram removidos ou que um arquivo está livre de malware.
Os seguintes limites de reconhecimento foram observados com as versões de dependências fixadas mostradas no código. “Aceito” significa armazenado como bytes originais privados, não aprovado para entrega pública:
| Entrada | Node | Flask | FileInfo do PHP 8.4.26 | Rails com file 5.46 / 5.48 |
|---|---|---|---|---|
| PNG comum, JPEG com metadados, GIF | Aceito | Aceito | Aceito | Aceito / aceito |
| JPEG MPF/MPO válido com várias imagens | Aceito como JPEG | 400: o Pillow informa MPO, fora da lista de formatos retornados permitidos | Aceito como JPEG | Aceito / aceito |
| APNG compacto | 400: reconhecido como APNG | Aceito como PNG | Aceito como PNG | Aceito / 422: incompatibilidade de MIME |
| APNG com 12 KB de metadados antes de seu marcador de animação | Aceito como PNG | Aceito como PNG | Aceito como PNG | Aceito / 422: incompatibilidade de MIME |
| APNG com 40 KB ou 90 KB antes desse marcador | Aceito como PNG | Aceito como PNG | Aceito como PNG | Aceito / aceito |
O Node examina apenas os primeiros 4.100 bytes. O file 5.48 tem uma
regra magic de PNG
que procura o marcador de animação em 16.384 bytes a partir do deslocamento 33. Mover um marcador para
além de uma janela de busca altera o reconhecimento sem mudar se a imagem é animada. Esses
manipuladores não impõem uma proibição geral de animações. A recusa de MPO pelo Flask também é um
limite da política, não uma prova de que o JPEG está corrompido; consulte o suporte a MPO do Pillow.
A cardinalidade multipart tem uma armadilha própria: os campos interpretados não são uma contagem de
todas as partes enviadas. O Node examina no máximo duas partes de arquivo e três partes no total,
descarta campos de texto e recusa vários arquivos visíveis em seu mapa de dados interpretados. Um
arquivo de outro campo além do limite de partes do parser pode ser omitido enquanto o myfile visível é armazenado com 201. O Flask
permite duas partes no total, conta também as partes de texto e retorna 413 acima desse limite;
arquivos duplicados visíveis retornam 400. Seu limite padrão de memória de formulário continua sendo
500.000 bytes, separado dos limites de arquivo e corpo.
O max_file_uploads=2 mostrado para o PHP processa até duas partes de arquivo. Partes escalares myfile duplicadas
se reduzem à última processada, e partes de arquivo posteriores podem ser ignoradas. De modo
semelhante, o Rails usa o último user[myfile] interpretado e ignora outros campos excluídos da entrada
permitida do controlador. No host Rack testado, 127 partes de arquivo e 4.095 partes no total foram
aceitas; atingir 128 partes de arquivo ou 4.096 partes no total causou uma exceção do parser. Essas
políticas limitadas aos campos interpretados não prometem recusar toda duplicação nos dados brutos
ou todo arquivo extra. Envie um arquivo e nenhum campo desnecessário para esta tarefa.
Soluções
Use primeiro uma imagem conhecida e decodificável, depois altere uma entrada por vez: o nome do campo, o tamanho do corpo, o conteúdo ou a permissão do destino. O Node também tem um temporizador de inatividade de upload de dez segundos e uma configuração de tempo limite de requisição HTTP de 15 segundos. Uma requisição lenta pode falhar abaixo do limite de tamanho; essas configurações não garantem um prazo exato de resposta medido pelo relógio.
Para falhas de validação no Rails, o controlador mostrado registra apenas códigos de motivo de
errors.details,
sob o prefixo “Upload validation rejected”. Consulte esse registro depois de um 422 para distinguir
recusas por tipo de conteúdo, falsificação e tamanho. Não registre os bytes enviados, nomes de
arquivos, cookies de sessão ou tokens.
Inspecione o estado do Rails antes de tentar novamente após um erro de armazenamento. Uma recusa na validação mantém a referência e os bytes do anexo anterior. Uma falha posterior de gravação é diferente: o Active Storage salva a referência antes do upload após o commit. No serviço Disk testado com o adaptador de tarefas inline, uma falha de gravação forçada retornou 500 com uma nova referência confirmada apontando para um arquivo ausente. O blob anterior e seus bytes permaneceram, mas já não eram o anexo atual do usuário. Consulte o usuário novamente e verifique o arquivo real ou o download desse blob; depois, inspecione o blob anterior e sua fila de tarefas. Um 500 não comprova que houve rollback; corrija a causa no armazenamento e reconcilie a referência antes de outra atualização.
Testar APIs de upload de arquivos com o Postman
Use primeiro a demonstração Node ou Flask em execução. Estas etapas foram testadas no cliente desktop
Linux do Postman 12.31.7; o PHP usa o mesmo campo com sua URL /upload.php. Para o Rails, use o
formulário gerado pela aplicação, a menos que você também forneça a sessão autenticada e o token CSRF.
- Crie uma requisição POST para
http://127.0.0.1:3000/upload. - Abra Body e selecione form-data.
- Adicione
myfile, escolha File como seu tipo e selecione uma imagem local com o seletor de arquivos do sistema operacional. - Deixe o cabeçalho Content-Type multipart a cargo do Postman para que ele inclua o delimitador correspondente.
- Clique em Send. Espere 201 e um novo arquivo armazenado; depois, compare seus bytes com a imagem selecionada.
- Altere apenas o campo para
wrongfielde envie novamente. Node, Flask e PHP retornam 400 sem adicionar um arquivo armazenado.
Para uma verificação pela linha de comando, coloque o PNG válido escolhido em sample.png no diretório do shell que executará o comando.
Use cURL com suporte a --fail-with-body; este comando foi testado com cURL 8.5.0:
curl --fail-with-body --silent --show-error -F "myfile=@./sample.png" http://127.0.0.1:3000/upload
Uma resposta de sucesso informa que o manipulador concluiu a gravação. Inspecione também private-uploads:
compare todos os bytes armazenados e abra a imagem armazenada com um decodificador independente. Uma
resposta, por si só, não pode dizer se você inspecionou o resultado correto. As demonstrações mantêm
os originais aceitos; pare o servidor e remova apenas seus próprios uploads de teste quando terminar.
Práticas recomendadas de segurança para uploads de arquivos
Mantenha os originais fora das raízes de arquivos estáticos ou documentos. Os exemplos Node e PHP
usam um novo subdiretório privado com nome definido pelo servidor para cada upload; o Flask usa a
criação exclusiva de um arquivo com nome aleatório definido pelo servidor. Requisições aceitas
repetidas mantêm resultados separados. Nenhum deles usa o nome de arquivo enviado como caminho de
armazenamento, portanto mudar o nome de uma foto para photo.php não cria um script PHP com esse nome.
Reconhecimento, decodificação, varredura de malware e publicação são decisões diferentes. Um cabeçalho reconhecível pode permanecer intacto mesmo com danos em uma parte posterior da imagem; os exemplos Node e PHP podem armazenar esse arquivo. Mesmo a verificação do Pillow não é uma decodificação completa. Antes da entrega pública, use uma etapa isolada de processamento que decodifique completamente o conteúdo compatível, aplique limites de recursos, produza o resultado pretendido e siga sua política de varredura de segurança. Mantenha o original privado até que essas etapas terminem. Esses manipuladores locais não implementam um scanner nem uma etapa de publicação.
Técnicas avançadas de upload de arquivos
Se seu problema real é retomar transferências interrompidas ou mostrar o progresso do upload, continue com o artigo otimização de uploads de arquivos em aplicações web. O artigo aborda um contrato diferente entre cliente e gateway; não fornece um cliente diretamente substituível para esses endpoints multipart. Para uma integração Laravel e Vue baseada em sessão, use o guia de upload do Laravel.
Conclusão e recursos adicionais
Para um upload multipart que falhou, mantenha uma requisição mínima bem-sucedida e compare com ela uma condição alterada por vez. O guia de upload do Flask, a documentação de upload do PHP e o guia do Active Storage explicam os contratos nativos por trás destes exemplos.
