Baixar imagem em Java com timeout, limite de tamanho e verificação do arquivo
Baixe imagens em Java sem congelar a interface: tempo limite de conexão e leitura, teto de bytes, assinatura do arquivo e gravação atômica que não estraga a foto antiga.
O catálogo desktop da distribuidora baixa a foto de cada produto direto do site do fornecedor. Num dia normal, funciona. Num dia ruim, um servidor responde devagar, a janela congela por dois minutos e, quando volta, o arquivo cafe-500g.jpg é na verdade uma página HTML de erro. A resposta curta: baixar imagem é uma conversa HTTP com prazo e com desconfiança. Defina tempo limite de conexão e de leitura, limite o tamanho, confira os primeiros bytes do arquivo e grave num temporário que só substitui o final quando tudo deu certo. E nunca faça isso na thread da interface.
O código deste guia usa só a biblioteca padrão do Java e cabe numa classe. Ele serve para um aplicativo desktop e, com os cuidados extras do fim, para um servidor.
Por que a linha única dá problema
O exemplo clássico abre a URL e copia o stream para um arquivo. Ele não diz quanto tempo esperar, então um servidor que aceita a conexão e não manda nada segura a thread indefinidamente. Ele não olha o status HTTP, então uma página 404 vira um ".jpg". E ele não limita nada, então um endereço que devolve 2 GB ocupa disco ou memória até acabar.
Há ainda a extensão. O link pode terminar em .png e devolver uma página de login, porque o fornecedor passou a exigir autenticação. A extensão é parte do nome, não prova do conteúdo. O que identifica o formato são os primeiros bytes: PNG começa com 89 50 4E 47, JPEG com FF D8 FF e WebP com RIFF seguido de WEBP a partir do byte 8.
Como baixar com segurança passo a passo
- Aceite só
httpse transforme a string emURI, para que um endereço malformado falhe cedo. - Defina tempo limite de conexão e de leitura.
- Recuse status diferente de 200 e tipo de conteúdo que não comece com
image/. - Se o servidor declarar o tamanho, recuse acima do teto antes de ler.
- Leia em blocos, contando bytes, e pare ao passar do teto.
- Confira a assinatura dos primeiros bytes.
- Grave num arquivo temporário na mesma pasta e só então mova para o nome final.
A classe completa
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
public final class BaixadorDeImagem {
private static final int MAX_BYTES = 5 * 1024 * 1024;
public static Path baixar(String endereco, Path destino) throws IOException {
URI uri = URI.create(endereco);
if (!"https".equalsIgnoreCase(uri.getScheme())) {
throw new IOException("Só aceito https: " + endereco);
}
HttpURLConnection con = (HttpURLConnection) uri.toURL().openConnection();
con.setConnectTimeout(5_000);
con.setReadTimeout(10_000);
con.setRequestProperty("Accept", "image/png,image/jpeg,image/webp");
try {
int status = con.getResponseCode();
if (status != 200) {
throw new IOException("HTTP " + status + " em " + endereco);
}
String tipo = con.getContentType();
if (tipo == null || !tipo.toLowerCase().startsWith("image/")) {
throw new IOException("Conteúdo não é imagem: " + tipo);
}
long declarado = con.getContentLengthLong();
if (declarado > MAX_BYTES) {
throw new IOException("Imagem grande demais: " + declarado + " bytes");
}
Path pasta = destino.toAbsolutePath().getParent();
Path temp = Files.createTempFile(pasta, "img-", ".part");
try (InputStream in = con.getInputStream();
OutputStream out = Files.newOutputStream(temp)) {
byte[] buf = new byte[8192];
byte[] cabecalho = new byte[12];
int lidos = 0;
int n;
while ((n = in.read(buf)) != -1) {
if (lidos < 12) {
System.arraycopy(buf, 0, cabecalho, lidos, Math.min(n, 12 - lidos));
}
lidos += n;
if (lidos > MAX_BYTES) {
throw new IOException("Passou de " + MAX_BYTES + " bytes");
}
out.write(buf, 0, n);
}
if (!assinaturaValida(cabecalho)) {
throw new IOException("Os bytes não são PNG, JPEG nem WebP");
}
} catch (IOException e) {
Files.deleteIfExists(temp);
throw e;
}
return Files.move(temp, destino,
StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
} finally {
con.disconnect();
}
}
static boolean assinaturaValida(byte[] h) {
boolean png = (h[0] & 0xFF) == 0x89 && h[1] == 'P' && h[2] == 'N' && h[3] == 'G';
boolean jpeg = (h[0] & 0xFF) == 0xFF && (h[1] & 0xFF) == 0xD8 && (h[2] & 0xFF) == 0xFF;
boolean webp = h[0] == 'R' && h[1] == 'I' && h[2] == 'F' && h[3] == 'F'
&& h[8] == 'W' && h[9] == 'E' && h[10] == 'B' && h[11] == 'P';
return png || jpeg || webp;
}
}
Por que HttpURLConnection e não HttpClient
O HttpClient do Java 11 é a API mais moderna e é a escolha certa para a maioria das chamadas. Mas o tempo limite da requisição dele cobre a espera pela resposta, não cada leitura do corpo. Aqui, o risco é o servidor que começa a responder e para no meio. O setReadTimeout do HttpURLConnection resolve exatamente isso: se nenhuma leitura recebe dados em 10 segundos, ela lança SocketTimeoutException, que é um IOException e cai no mesmo tratamento.
Gravação que não estraga a foto boa
O temporário é criado na mesma pasta do destino de propósito. Mover dentro do mesmo sistema de arquivos é uma renomeação, e com ATOMIC_MOVE o arquivo final ou é o antigo inteiro ou é o novo inteiro. Se a rede cair no meio, o cafe-500g.jpg que já estava lá continua intacto, e o .part é apagado no catch.
Como chamar sem congelar a janela
Num aplicativo Swing, a thread de eventos desenha a tela. Se ela espera a rede, a janela para de responder. Rode o download em outra thread e volte para a thread de eventos só para atualizar a interface:
CompletableFuture
.supplyAsync(() -> {
try {
return BaixadorDeImagem.baixar(url, destino);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
})
.whenComplete((caminho, erro) -> SwingUtilities.invokeLater(() -> {
if (erro != null) {
status.setText("Não consegui baixar a foto: " + erro.getCause().getMessage());
} else {
mostrarFoto(caminho);
}
}));
Em JavaFX, o equivalente de SwingUtilities.invokeLater é Platform.runLater. Se várias telas pedem a mesma imagem, guarde o arquivo baixado com o endereço e a data, e só baixe de novo quando o cache vencer.
Quando quem baixa é o servidor
Se o seu servidor baixa uma URL informada pelo usuário, o risco muda: alguém pode passar o endereço de um serviço interno ou do serviço de metadados da nuvem, e o seu servidor vai buscar. Antes de conectar, resolva o host com InetAddress.getAllByName e recuse endereços em que isLoopbackAddress, isSiteLocalAddress, isLinkLocalAddress ou isAnyLocalAddress sejam verdadeiros. Desligue o redirecionamento automático com setInstanceFollowRedirects(false) e valide cada salto, porque um redirecionamento pode apontar para a rede interna.
Para guardar a imagem, o mais seguro é decodificá-la e gravá-la de novo, descartando o arquivo original, como em arquivo enviado vira imagem de novo. Lembre que o ImageIO padrão do JDK lê PNG e JPEG, mas não WebP sem biblioteca extra. Para gerar miniaturas, veja redimensionar imagem no servidor.
Erros comuns
- Não definir tempo limite. Um servidor lento segura a thread para sempre e, se for a da interface, o programa parece travado.
- Confiar na extensão da URL. Uma página de login é salva como ".jpg" e a tela mostra imagem quebrada.
- Ler tudo para a memória sem teto. Um endereço que devolve um arquivo enorme derruba o programa com falta de memória.
- Gravar direto no nome final. Um download cortado substitui a foto boa por meio arquivo.
- Ignorar o status HTTP. Respostas 403 e 404 viram arquivos de imagem inválidos no disco.
- Baixar URL de usuário no servidor sem filtrar endereços internos. O seu servidor vira ponte para a rede privada.
Checklist
- Há tempo limite de conexão e de leitura em todo download.
- Status diferente de 200 e tipo diferente de
image/são recusados. - Existe um teto de bytes verificado durante a leitura.
- Os primeiros bytes são conferidos contra as assinaturas aceitas.
- O arquivo é gravado em temporário e movido de forma atômica.
- Nenhum download roda na thread da interface.
- No servidor, endereços internos e redirecionamentos são validados.
Para o princípio geral de prazos em chamadas externas, veja timeout antes do infinito. E faça o teste que ninguém faz: desligue a rede no meio de um download do seu programa. Ele mostra uma frase de erro e mantém a foto anterior, ou congela e deixa um arquivo pela metade?
Referência: https://pt.stackoverflow.com/questions/2673/como-pegar-imagens-na-internet-atraves-do-java