RestClient is an HTTP client that offers a modern API built on top of underlying HTTP client libraries such as the native HTTP client from JDK 11 and Apache HttpComponents. As the existing RestTemplate client is planned for deprecation, RestClient can be considered its modern replacement. (See more details: The state of HTTP clients in Spring)
In this article, we'll see how to configure the client for some common use cases.
Creating Client Bean with Common Parameters:
In some cases, we may need a client instance configured for a specific API, and in such cases, we don’t want to send common request parameters with every single request. In the following example, the client bean is configured with a base URL and a default header that will be included in every request. It sends a dynamic variable in the URL also.
@Bean
public RestClient restClient() {
return RestClient.builder()
.baseUrl("https://example.com/{vendor}")
.defaultUriVariables(Map.of("vendor", "MyVendor"))
.defaultHeaders(headers -> {
headers.add("X-Platform", "Company");
})
.build();
}
Exception Handling
By default, RestClient throws a subclass of RestClientException when retrieving a response with HTTP/4xx or HTTP/5xx status code. You can override the behavior as shown below.
You can log the error, and throw a custom exception for any HTTP errors using HttpStatusCode::isError, or for server-side errors using HttpStatusCode::is5xxServerError, as shown below. :
@Bean
public RestClient restClient() {
return RestClient.builder()
.defaultStatusHandler(HttpStatusCode::is5xxServerError, ((request, response) -> {
logger.error(
"Api request was failed. Response status: {}, body: {}",
response.getStatusCode(),
new String(response.getBody().readAllBytes())
);
throw new ApiError("Something went wrong");
}))
.build();
}
Or you can simply make the client silent in case any HTTP errors occur.
@Bean
public RestClient restClient() {
return RestClient.builder()
.defaultStatusHandler((response) -> true)
.build();
}
Setting Up Client Connection and Read Timeouts
If you're a Spring Boot user, you can simply configure the client using properties or configuration beans.
In order to configure the timeouts, you can use the following properties
spring.http.client.connect-timeout=30s
spring.http.client.read-timeout=5s
Or you can define a bean in your configuration classes as shown below:
@Bean
public RestClient restClient(RestClient.Builder builder) {
ClientHttpRequestFactorySettings settings = ClientHttpRequestFactorySettings
.defaults()
.withConnectTimeout(Duration.ofMillis(30000))
.withReadTimeout(Duration.ofMillis(5000));
ClientHttpRequestFactory requestFactory = ClientHttpRequestFactoryBuilder.detect().build(settings);
return builder.requestFactory(requestFactory)
.build();
}
If you're not using Spring Boot...
Because RestClient executes HTTP calls through underlying HTTP client libraries, you can setup connection and read timeouts using ClientHttpRequestFactory implementations provided by Spring Framework.
Spring Framework provides the following ClientHttpRequestFactory implementations:
- SimpleClientHttpRequestFactory as a simple default
- JdkClientHttpRequestFactory for Java’s HttpClient (JDK11+)
- HttpComponentsClientHttpRequestFactory for use with Apache HTTP Components HttpClient
- JettyClientHttpRequestFactory for Jetty’s HttpClient
- ReactorNettyClientRequestFactory for Reactor Netty’s HttpClient
If you haven’t specified a request factory while building the RestClient bean, it will use the Apache or Jetty HttpClient if they are available on the classpath. Otherwise, if the java.net.http module is loaded, it will use Java’s HttpClient. Finally, it will fall back to the simple default implementation.
You can change the client connection and read timeouts for the default as shown below:
@Bean
public RestClient restClient() {
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
requestFactory.setConnectTimeout(Duration.ofMillis(30000));
requestFactory.setReadTimeout(Duration.ofMillis(5000));
return RestClient.builder()
.requestFactory(requestFactory)
.build();
}
Because SimpleClientHttpRequestFactory executes HTTP calls using java.net.HttpURLConnection, you may prefer to use JdkClientHttpRequestFactory or another implementation due to protocol- or performance-related limitations. You can setup the timeouts as shown below in such cases:
@Bean
public RestClient restClient() {
HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofMillis(30000))
.build();
JdkClientHttpRequestFactory requestFactory = new JdkClientHttpRequestFactory(client);
requestFactory.setReadTimeout(Duration.ofMillis(5000));
return RestClient.builder()
.requestFactory(requestFactory)
.build();
}
Changing Underlying HTTP Client Library
If you're a Spring Boot user and the auto-detected HTTP client does not meet your needs, you can simply change it through spring.http.client.factory property.
# available options: http-components, jetty, reactor, jdk, and simple.
spring.http.client.factory=jdk
Or you can create a bean like shown below in your configuration class for more complex customizations
@Bean
public RestClient restClient(RestClient.Builder builder) {
ClientHttpRequestFactorySettings settings = ClientHttpRequestFactorySettings
.defaults()
.withConnectTimeout(Duration.ofMillis(30000))
.withReadTimeout(Duration.ofMillis(5000));
ClientHttpRequestFactory requestFactory = ClientHttpRequestFactoryBuilder
.jdk()
.build(settings);
return builder.requestFactory(requestFactory)
.defaultStatusHandler((response) -> true)
.build();
}
Client Request/Response Logging
While you can log request and response transactions by changing log level of the underlying HTTP client package, you can create a ClientHttpRequestInterceptor implementation as shown below.
@Component
public class RequestLoggerInterceptor implements ClientHttpRequestInterceptor {
private static final Logger logger = LoggerFactory.getLogger(RequestLoggerInterceptor.class);
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
logRequest(request, body);
ClientHttpResponse response = execution.execute(request, body);
logResponse(request, response);
return response;
}
private void logRequest(HttpRequest request, byte[] body) {
logger.info("Request: {} {}", request.getMethod(), request.getURI());
if (body != null && body.length > 0) {
logger.info("Request body: {}", new String(body, StandardCharsets.UTF_8));
}
}
private void logResponse(HttpRequest request, ClientHttpResponse response) throws IOException {
logger.info("Response status: {}", response.getStatusCode());
byte[] responseBody = response.getBody().readAllBytes();
if (responseBody.length > 0) {
logger.info("Response body: {}", new String(responseBody, StandardCharsets.UTF_8));
}
}
}
And register the interceptor to the client while building the RestClient bean.
@Bean
public RestClient restClient1(RestClient.Builder builder, RequestLoggerInterceptor loggerInterceptor) {
return builder
.requestInterceptor(loggerInterceptor)
.build();
}
Although the client throws an exception when it receives a response with an HTTP 4xx or 5xx status code, it will still execute the interceptor and continue logging.
2025-10-30T09:11:28.932+02:00 INFO 101069 --- [ main] org.example.RequestLoggerInterceptor : Request: GET http://localhost:8080/
2025-10-30T09:11:28.971+02:00 INFO 101069 --- [ main] org.example.RequestLoggerInterceptor : Response status: 400 BAD_REQUEST
2025-10-30T09:11:28.971+02:00 INFO 101069 --- [ main] org.example.RequestLoggerInterceptor : Response body: Invalid request
Exception in thread "main" org.springframework.web.client.HttpClientErrorException$BadRequest: 400 Bad Request: [no body]
at org.springframework.web.client.HttpClientErrorException.create(HttpClientErrorException.java:103)
at org.springframework.web.client.StatusHandler.lambda$defaultHandler$3(StatusHandler.java:86)
at org.springframework.web.client.StatusHandler.handle(StatusHandler.java:146)
...
Thanks for reading!
Top comments (0)