Uma chamada com RestClient no Spring Boot precisa de prazos definidos e tratamento separado para resposta HTTP de erro e falha de transporte. Se uma API parceira demora, a sua requisição fica ocupada esperando; se o parceiro responde 404, houve uma resposta válida do protocolo com significado próprio. Tratar as duas situações como “retornou null” esconde a causa e torna retries perigosos.
O RestClient é síncrono. Ele combina com serviços de fluxo imperativo, mas a chamada bloqueia a tarefa que a executa. Na documentação do Spring Framework 7, o RestTemplate aparece como deprecated em favor do RestClient, sem que isso signifique remoção imediata. A escolha entre esses clientes e o WebClient deve considerar o modelo da aplicação, conforme a referência oficial de clientes REST.
RestClient no Spring Boot: configuração sem perder o builder
Em uma aplicação Boot, injete o RestClient.Builder configurado pelo framework. Criar um cliente com RestClient.create() em qualquer lugar pode deixar de fora a autoconfiguração e os customizadores do Boot. Quando houver vários clientes, clonar o builder antes de configurá-lo ajuda a evitar que alterações de um cliente afetem construções seguintes que reutilizam o mesmo objeto.
Na documentação consultada do Boot 4.1.2, as propriedades globais usam spring.http.clients, no plural. O exemplo abaixo estabelece valores didáticos para a conexão e a leitura. Confirme os nomes na documentação da linha em uso: copiar propriedades de outra versão pode produzir um arquivo que parece correto e não altera o cliente esperado.
# application.properties, Spring Boot 4.1.2
spring.http.clients.connect-timeout=1s
spring.http.clients.read-timeout=2s
A referência do Boot para chamadas REST detalha o builder, a seleção do transporte e as customizações. Se você fornece uma request factory própria, precisa assumir também sua configuração. Não presuma que cada ajuste global continuará sendo aplicado a uma fábrica criada manualmente com outros limites.
Um serviço de catálogo, por exemplo, pode receber o builder por construtor e criar uma única instância reutilizável do cliente com a URL base da integração. Mantenha essa URL em configuração, valide o ambiente e não aceite um endereço arbitrário enviado pelo consumidor da API. A documentação de ConfigurationProperties no Spring Boot é uma leitura complementar para organizar esses parâmetros.
O que os timeouts cobrem e o que fica fora
O timeout de conexão controla uma etapa diferente do timeout de leitura. A semântica exata depende da implementação HTTP escolhida e de como ela aplica seus limites. DNS, TLS, espera por conexão de um pool e consumo do corpo da resposta merecem testes específicos quando fazem parte do risco da integração. Um único número no YAML não descreve necessariamente o prazo total de uma operação.
Considere um endpoint hipotético com orçamento de resposta de três segundos e duas chamadas sequenciais. Permitir dois segundos para cada chamada já pode ultrapassar esse orçamento antes de contar processamento local. Um retry acrescenta nova tentativa e eventual intervalo. A decisão deve considerar o tempo restante da operação, não apenas o timeout configurado isoladamente em cada cliente.
Há ainda uma diferença entre desistir de esperar e desfazer uma operação remota. Se o parceiro processou um pedido e a conexão caiu antes da resposta, o cliente não sabe automaticamente se o pedido foi criado. Repetir um POST pode duplicar o efeito. Use a estratégia de idempotência acordada com a API parceira e um mecanismo de consulta ou reconciliação quando o resultado ficar incerto.
Exemplo prático com servidor HTTP local
Este teste executável valida três comportamentos do RestClient: leitura de um corpo com status 200, propagação de um status 404 e timeout quando o servidor atrasa o envio da resposta. Ele usa o transporte JDK de forma explícita, sem subir uma aplicação Boot. Isso deixa claro qual cliente foi exercitado e evita depender de uma API externa durante o teste.
Crie um projeto Maven com o arquivo abaixo e salve a classe seguinte em src/main/java/RestClientDemo.java. A execução local usou OpenJDK 21.0.8, distribuição JBR, e Spring Framework 7.0.10. O servidor abre somente uma porta aleatória em localhost. Esses testes verificam comportamento, não medem o desempenho de um serviço em produção.
<?xml version="1.0" ?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>br.com.javalizando</groupId>
<artifactId>restclient-demo</artifactId>
<version>1.0</version>
<properties>
<maven.compiler.release>21</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>7.0.10</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.14.1</version>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.5.1</version>
</plugin>
</plugins>
</build>
</project>
import com.sun.net.httpserver.HttpServer;
import java.net.InetSocketAddress;
import java.net.http.HttpClient;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.concurrent.Executors;
import org.springframework.http.client.JdkClientHttpRequestFactory;
import org.springframework.web.client.ResourceAccessException;
import org.springframework.web.client.RestClient;
import org.springframework.web.client.RestClientResponseException;
public class RestClientDemo {
public static void main(String[] args) throws Exception {
var server = HttpServer.create(
new InetSocketAddress("127.0.0.1", 0), 0);
var workers = Executors.newCachedThreadPool();
server.setExecutor(workers);
server.createContext("/", exchange -> {
try (exchange) {
String path = exchange.getRequestURI().getPath();
if (path.equals("/lento")) {
try { Thread.sleep(800); }
catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
int status = path.equals("/ausente") ? 404 : 200;
byte[] body = "resposta-local"
.getBytes(StandardCharsets.UTF_8);
exchange.getResponseHeaders().add(
"Content-Type", "text/plain; charset=utf-8");
exchange.sendResponseHeaders(status, body.length);
exchange.getResponseBody().write(body);
} catch (java.io.IOException ignored) {
// O cliente pode fechar a conexão após o timeout.
}
});
server.start();
try {
var http = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(1)).build();
var factory = new JdkClientHttpRequestFactory(http);
factory.setReadTimeout(Duration.ofMillis(200));
var client = RestClient.builder().requestFactory(factory)
.baseUrl("http://127.0.0.1:"
+ server.getAddress().getPort()).build();
String body = client.get().uri("/ok")
.retrieve().body(String.class);
if (!"resposta-local".equals(body))
throw new AssertionError("Resposta inesperada");
try {
client.get().uri("/ausente").retrieve()
.toBodilessEntity();
throw new AssertionError("Esperava 404");
} catch (RestClientResponseException e) {
if (e.getStatusCode().value() != 404) throw e;
}
try {
client.get().uri("/lento").retrieve()
.toBodilessEntity();
throw new AssertionError("Esperava timeout");
} catch (ResourceAccessException e) {
if (!(e.getCause() instanceof
java.net.http.HttpTimeoutException)) throw e;
}
System.out.println("OK: corpo 200, status 404 e timeout HTTP");
} finally {
server.stop(0);
workers.shutdownNow();
}
}
}
mvn -q compile exec:java -Dexec.mainClass=RestClientDemo
OK: corpo 200, status 404 e timeout HTTPO teste lento espera 800 milissegundos antes de enviar os cabeçalhos, enquanto a request factory usa 200 milissegundos de timeout de leitura. A asserção verifica a exceção de timeout como causa da falha de acesso. Ela não exige que a execução termine em um milissegundo exato, porque o escalonamento e o ambiente introduzem variação. O teste não cobre lentidão durante streaming do corpo, DNS, TLS ou timeout de conexão.
O uso de RestClient.builder() nesse programa é intencional: trata-se de um teste independente, no qual criamos e configuramos o transporte. Na aplicação Boot, mantenha a injeção do builder e teste também o bean efetivo. Assim você verifica se o perfil, o customizador ou uma dependência no classpath alterou a fábrica que realmente atende às chamadas.
RestClient no Spring Boot: erros comuns na integração
Por padrão, a recuperação com retrieve() trata respostas 4xx e 5xx como erros. É possível personalizar esse mapeamento, mas a regra deve refletir a integração. Um 404 em uma consulta de produto pode significar ausência esperada; em outro endpoint, pode indicar URL montada incorretamente. Traduzir todo 404 para “produto não existe” sem avaliar o contrato esconderia um defeito de configuração.
Falhas de acesso, como a do teste de timeout, não carregam necessariamente um status HTTP remoto. A abordagem mais clara preserva essa distinção na camada de integração e deixa a camada web decidir a resposta da sua API. O artigo sobre exceções por camada no Spring Boot ajuda a evitar um handler genérico que mistura falha do parceiro, erro de negócio e bug local.
Em um segundo cenário hipotético, uma API de frete retorna 503 durante um pico. Repetir a chamada imediatamente em todas as instâncias aumenta a carga no momento em que o parceiro está mais frágil. Antes de adicionar retry, defina quais falhas são transitórias, quantas tentativas cabem no orçamento e como a equipe limitará a concorrência. Para operações com efeito, a segurança da repetição precisa ser comprovada pelo contrato.
Outro erro comum é registrar o corpo inteiro de uma falha remota. A resposta pode conter dados pessoais ou informações operacionais. Prefira informações suficientes para diagnóstico, como integração, duração, classe da falha, status quando houver e identificador de correlação permitido. Não coloque tokens, cabeçalhos de autorização ou payloads completos em logs por conveniência.
Quando usar mock e quando testar transporte real
O MockRestServiceServer é útil para testar contratos e comportamento isolado, mas substitui a request factory do cliente. Por isso, um teste que devolve uma resposta simulada por esse mecanismo não comprova o timeout da fábrica de produção. A documentação de testes de clientes do Spring recomenda servidores mock HTTP para exercitar de forma mais completa a camada de transporte.
Um stub local, como o do exemplo, permite atrasar a resposta e observar o cliente real. Para uma suíte maior, ferramentas dedicadas podem facilitar cenários de desconexão, corpo incompleto e respostas sequenciais. A escolha deve acompanhar o risco: um teste simples de montagem de URL não precisa reproduzir TLS, mas uma política de timeout não deve depender exclusivamente de um mock que troca o transporte.
Organize essas verificações junto da estratégia descrita no guia de testes no Spring Boot. Mantenha poucos cenários de rede bem escolhidos e asserções que expliquem a falha. Um teste instável baseado em um limite de tempo apertado e sem tolerância pode esconder regressões entre falsos alarmes.
FAQ sobre RestClient e timeout
RestClient é assíncrono?
Não. Seu modelo é síncrono e a tarefa chamadora espera pela operação. Se a aplicação usa um fluxo reativo, avalie o WebClient dentro desse modelo. Trocar apenas o nome do cliente não transforma uma cadeia bloqueante em uma cadeia não bloqueante.
Posso usar o mesmo timeout para todas as integrações?
Um padrão global ajuda como ponto de partida, mas integrações podem ter orçamentos e contratos diferentes. Defina exceções de forma explícita e teste o cliente efetivo. Uma exportação longa e uma consulta usada no checkout dificilmente têm a mesma tolerância de espera.
O teste do exemplo valida as propriedades do Boot?
Não. Ele valida o RestClient com uma fábrica JDK configurada no código. Para verificar as propriedades, carregue o contexto Boot da aplicação e exercite o cliente injetado contra um servidor local com atraso controlado, usando o perfil de teste correto.
Conclusão: teste o limite que a integração promete
O próximo passo é escolher uma chamada externa da aplicação, registrar seu orçamento de resposta e testar tanto um erro HTTP quanto uma demora de transporte. Confirme qual request factory está em uso e se o retry cabe no prazo sem duplicar efeitos. Como leitura complementar, os textos sobre configuração, exceções e testes ligados acima ajudam a transformar essas decisões em um contrato que a equipe consegue manter.
