Home โ€บ Java โ€บ RestClient in Spring Boot 3: The Modern HTTP Client Guide
Intermediate 6 min · July 14, 2026

RestClient in Spring Boot 3: The Modern HTTP Client Guide

Learn how to use RestClient in Spring Boot 3.2+ with production insights, debugging tips, and real-world incident stories.

N
Naren Founder & Principal Engineer

20+ years shipping production Java in banking & fintech. Notes here come from systems that actually shipped.

Follow
Production
production tested
July 15, 2026
last updated
2,398
articles · all by Naren
Before you start⏱ 15-20 min read
  • Java 17 or later
  • Spring Boot 3.2 or later
  • Basic understanding of HTTP and REST APIs
  • Familiarity with Spring's dependency injection
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • RestClient is the modern replacement for RestTemplate, introduced in Spring Framework 6.1 (Spring Boot 3.2).
  • It offers a fluent, immutable API with better error handling and reactive integration.
  • Use RestClient for synchronous HTTP calls; for async, prefer WebClient.
  • Migrate from RestTemplate by wrapping it with RestClient.builder(restTemplate).
  • Key features: interceptors, status handlers, and exchange filter functions.
โœฆ Definition~90s read
What is A Complete Guide to RestClient in Spring Boot 3?

RestClient is a synchronous HTTP client in Spring Framework 6.1+ that provides a fluent, immutable API for making HTTP requests, designed as a modern replacement for RestTemplate.

โ˜…
Think of RestClient as a modern take on ordering a pizza.
Plain-English First

Think of RestClient as a modern take on ordering a pizza. With RestTemplate, you had to call the pizza shop, wait on hold, and hope they got your order rightโ€”it was messy and error-prone. RestClient is like using a sleek app: you fill in your order (the request), press send, and get a clear confirmation (the response) with options to handle wrong toppings (errors) gracefully. Itโ€™s simpler, faster, and less likely to mess up.

If youโ€™ve been building Spring Boot applications for a while, youโ€™ve probably used RestTemplate. It was the go-to synchronous HTTP client for years. But letโ€™s be honest: RestTemplate is a legacy beast. Itโ€™s bloated, callback-heavy, and its error handling is a nightmare in production. Iโ€™ve lost count of how many times Iโ€™ve seen teams struggle with RestTemplateโ€™s non-intuitive exception hierarchy or its tight coupling to the old Apache HttpClient.

Enter RestClient, introduced in Spring Framework 6.1 (Spring Boot 3.2). Itโ€™s the HTTP client Spring should have had from the start. RestClient is a synchronous, fluent, and immutable client that borrows the best ideas from WebClient (reactive) but keeps things simple for blocking calls. Itโ€™s not just a faceliftโ€”itโ€™s a complete rethinking of how we make HTTP requests in Spring.

In this guide, Iโ€™ll walk you through everything you need to know: from basic usage to advanced patterns, error handling, testing, and migration from RestTemplate. Iโ€™ll also share war stories from production where RestClient saved the dayโ€”or where it could have if weโ€™d used it sooner. By the end, youโ€™ll never want to touch RestTemplate again.

Why RestClient? The End of RestTemplate

RestTemplate has been deprecated since Spring 5.0, yet many teams still cling to it. I get itโ€”migration is painful. But RestClient is not just a new coat of paint; itโ€™s a fundamentally better design. Hereโ€™s why:

  1. Fluent API: RestClient uses a builder pattern thatโ€™s intuitive. No more chaining exchange() with anonymous classes.
  2. Immutability: Once built, RestClient is thread-safe and immutable. No accidental state mutation.
  3. Better Error Handling: RestClient provides onStatus and defaultStatusHandler to handle HTTP statuses cleanly. No more catching HttpClientErrorException and parsing the response body manually.
  4. Reactive Integration: RestClient can be used alongside WebClient, sharing the same infrastructure.
  5. Testability: RestClient works seamlessly with MockRestServiceServer, making unit tests a breeze.

Letโ€™s see a quick comparison. RestTemplate code to make a GET request and parse JSON:

``java // Old RestTemplate ResponseEntity<List<Payment>> response = restTemplate.exchange( "https://api.example.com/payments", HttpMethod.GET, null, new ParameterizedTypeReference<List<Payment>>() {} ); ``

``java // New RestClient List<Payment> payments = restClient.get() .uri("https://api.example.com/payments") .retrieve() .body(new ParameterizedTypeReference<>() {}); ``

Cleaner, right? And thatโ€™s just the beginning.

RestClientBasicExample.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import org.springframework.web.client.RestClient;

public class RestClientBasicExample {
    public static void main(String[] args) {
        RestClient restClient = RestClient.create();

        String result = restClient.get()
                .uri("https://api.example.com/health")
                .retrieve()
                .body(String.class);

        System.out.println("Health check: " + result);
    }
}
Output
Health check: {"status":"UP"}
๐Ÿ”ฅDon't Use RestClient.create() in Production
๐Ÿ“Š Production Insight
I once replaced RestTemplate with RestClient in a payment gateway service. The error handling alone saved us from three Sev-2 incidents in the first month. The old code had a generic catch-all that swallowed 401 errors; RestClient's onStatus made it explicit.
๐ŸŽฏ Key Takeaway
RestClient is the modern, fluent replacement for RestTemplate. Use it for all new synchronous HTTP calls.

Building and Configuring RestClient

A well-configured RestClient is crucial for production. Hereโ€™s how to set it up properly.

Basic Builder

``java RestClient restClient = RestClient.builder() .baseUrl("https://api.example.com") .defaultHeader("Authorization", "Bearer " + token) .defaultHeader("Accept", "application/json") .build(); ``

Timeouts โ€“ Never skip these. Use a custom ClientHttpRequestFactory like HttpComponentsClientHttpRequestFactory (Apache HttpClient) or JdkClientHttpRequestFactory (Java 11+).

```java import org.apache.hc.client5.http.impl.classic.HttpClients; import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;

PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(); connectionManager.setMaxTotal(100); connectionManager.setDefaultMaxPerRoute(20);

HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory( HttpClients.custom() .setConnectionManager(connectionManager) .build() ); requestFactory.setConnectTimeout(5000); requestFactory.setReadTimeout(10000);

RestClient restClient = RestClient.builder() .requestFactory(requestFactory) .build(); ```

Error Handling โ€“ Use onStatus for specific HTTP statuses and defaultStatusHandler for a catch-all.

``java restClient.get() .uri("/payments/{id}", paymentId) .retrieve() .onStatus(status -> status.value() == 404, (request, response) -> { throw new PaymentNotFoundException("Payment not found: " + paymentId); }) .onStatus(HttpStatusCode::is4xxClientError, (request, response) -> { throw new ClientException(response.getStatusText()); }) .body(Payment.class); ``

Interceptors โ€“ Great for logging, retries, or adding correlation IDs.

```java import org.springframework.http.client.ClientHttpRequestInterceptor;

ClientHttpRequestInterceptor loggingInterceptor = (request, body, execution) -> { System.out.println("Request: " + request.getMethod() + " " + request.getURI()); return execution.execute(request, body); };

RestClient restClient = RestClient.builder() .requestInterceptor(loggingInterceptor) .build(); ```

RestClientConfig.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestClient;

@Configuration
public class RestClientConfig {

    @Bean
    public RestClient paymentServiceClient() {
        HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
        factory.setConnectTimeout(5000);
        factory.setReadTimeout(10000);

        return RestClient.builder()
                .baseUrl("https://payments.example.com")
                .requestFactory(factory)
                .defaultHeader("Accept", "application/json")
                .build();
    }
}
โš  Don't Forget Connection Pooling
๐Ÿ“Š Production Insight
A common mistake is using RestClient.builder().build() without customizing the request factory. In a high-throughput system, this leads to connection exhaustion. Always explicitly set a pooled connection manager.
๐ŸŽฏ Key Takeaway
Always configure timeouts, connection pooling, and error handlers when building RestClient for production.

Making Requests: GET, POST, PUT, DELETE

RestClientโ€™s fluent API makes all HTTP methods straightforward. Let's cover the common patterns.

GET โ€“ Retrieve a resource.

``java Payment payment = restClient.get() .uri("/payments/{id}", paymentId) .retrieve() .body(Payment.class); ``

``java ResponseEntity<List<Payment>> response = restClient.get() .uri("/payments?status={status}", "completed") .retrieve() .toEntity(new ParameterizedTypeReference<>() {}); ``

POST โ€“ Create a resource.

``java Payment newPayment = new Payment(amount, currency); Payment created = restClient.post() .uri("/payments") .body(newPayment) .retrieve() .body(Payment.class); ``

PUT โ€“ Update a resource.

``java Payment updatedPayment = new Payment(amount, currency); restClient.put() .uri("/payments/{id}", paymentId) .body(updatedPayment) .retrieve() .toBodilessEntity(); ``

DELETE โ€“ Remove a resource.

``java restClient.delete() .uri("/payments/{id}", paymentId) .retrieve() .toBodilessEntity(); ``

Exchange โ€“ For full control over the response (e.g., to read headers).

``java String etag = restClient.get() .uri("/payments/{id}", paymentId) .exchange((request, response) -> { if (response.getStatusCode().is2xxSuccessful()) { return response.getHeaders().getETag(); } else { throw new RuntimeException("Failed"); } }); ``

Note: exchange gives you access to the raw ClientHttpResponse, but you must manage the response lifecycle (e.g., close the body).

RestClientRequests.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import org.springframework.web.client.RestClient;
import org.springframework.http.ResponseEntity;

public class RestClientRequests {
    private final RestClient restClient;

    public RestClientRequests(RestClient restClient) {
        this.restClient = restClient;
    }

    public Payment getPayment(String id) {
        return restClient.get()
                .uri("/payments/{id}", id)
                .retrieve()
                .body(Payment.class);
    }

    public Payment createPayment(Payment payment) {
        return restClient.post()
                .uri("/payments")
                .body(payment)
                .retrieve()
                .body(Payment.class);
    }

    public void deletePayment(String id) {
        restClient.delete()
                .uri("/payments/{id}", id)
                .retrieve()
                .toBodilessEntity();
    }
}
๐Ÿ’กUse toBodilessEntity for Void Responses
๐Ÿ“Š Production Insight
In a recent project, we had a bug where exchange was used for every GET call. The developer forgot to close the response body, causing memory leaks. Prefer retrieve for most cases.
๐ŸŽฏ Key Takeaway
RestClient supports all HTTP methods with a clean, fluent API. Use exchange only when you need low-level access.

Error Handling and Status Handlers

One of RestClientโ€™s best features is its declarative error handling. You can handle specific status codes or ranges without messy try-catch blocks.

Basic Error Handling

By default, RestClient throws RestClientException for 4xx and 5xx responses. But you can customize this.

``java Payment payment = restClient.get() .uri("/payments/{id}", id) .retrieve() .onStatus(status -> status.value() == 404, (request, response) -> { throw new PaymentNotFoundException(); }) .onStatus(HttpStatusCode::is5xxServerError, (request, response) -> { throw new ServiceUnavailableException(); }) .body(Payment.class); ``

Default Status Handler

``java RestClient restClient = RestClient.builder() .defaultStatusHandler(HttpStatusCode::isError, (request, response) -> { throw new CustomHttpException(response.getStatusCode(), response.getStatusText()); }) .build(); ``

Reading Error Response Body

Sometimes you need the error body. Use onStatus with a custom extractor:

``java .onStatus(HttpStatusCode::is4xxClientError, (request, response) -> { String errorBody = new String(response.getBody().readAllBytes(), StandardCharsets.UTF_8); throw new ClientException(response.getStatusCode(), errorBody); }) ``

Retry After

``java .onStatus(status -> status.value() == 429, (request, response) -> { String retryAfter = response.getHeaders().getFirst("Retry-After"); throw new RateLimitExceededException(retryAfter); }) ``

Warning: Avoid using onStatus for every possible status code. Keep it maintainable by grouping statuses into ranges.

RestClientErrorHandling.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import org.springframework.web.client.RestClient;
import org.springframework.http.HttpStatusCode;

public class RestClientErrorHandling {
    private final RestClient restClient;

    public RestClientErrorHandling(RestClient restClient) {
        this.restClient = restClient;
    }

    public Payment getPaymentSafe(String id) {
        return restClient.get()
                .uri("/payments/{id}", id)
                .retrieve()
                .onStatus(HttpStatusCode::is4xxClientError, (request, response) -> {
                    throw new ClientException("Client error: " + response.getStatusCode());
                })
                .onStatus(HttpStatusCode::is5xxServerError, (request, response) -> {
                    throw new ServerException("Server error: " + response.getStatusCode());
                })
                .body(Payment.class);
    }
}
โš  Don't Swallow Exceptions in onStatus
๐Ÿ“Š Production Insight
I once saw a team use onStatus that logged the error but didn't throw. The result: null returned from body(), causing a confusing NPE downstream. Always throw or return a fallback.
๐ŸŽฏ Key Takeaway
Use onStatus and defaultStatusHandler for clean, declarative error handling. Always throw a meaningful exception from handlers.

Testing RestClient with MockRestServiceServer

Testing HTTP clients is critical. Spring provides MockRestServiceServer for mocking RestClient calls without hitting real endpoints.

Setup

```java import org.springframework.test.web.client.MockRestServiceServer; import static org.springframework.test.web.client.match.MockRestRequestMatchers.; import static org.springframework.test.web.client.response.MockRestResponseCreators.;

@ExtendWith(MockitoExtension.class) class PaymentServiceTest {

private MockRestServiceServer mockServer; private PaymentService paymentService;

@BeforeEach void setUp() { RestClient restClient = RestClient.builder() .baseUrl("https://api.example.com") .build(); mockServer = MockRestServiceServer.bindTo(restClient).build(); paymentService = new PaymentService(restClient); } ```

Test a Successful GET

```java @Test void shouldReturnPaymentWhenFound() { String json = "{\"id\":\"123\",\"amount\":100.0}"; mockServer.expect(requestTo("/payments/123")) .andExpect(method(HttpMethod.GET)) .andRespond(withSuccess(json, MediaType.APPLICATION_JSON));

Payment payment = paymentService.getPayment("123");

assertEquals("123", payment.id()); assertEquals(100.0, payment.amount()); } ```

Test Error Handling

```java @Test void shouldThrowNotFoundWhen404() { mockServer.expect(requestTo("/payments/999")) .andRespond(withStatus(HttpStatus.NOT_FOUND));

assertThrows(PaymentNotFoundException.class, () -> { paymentService.getPayment("999"); }); } ```

Test Timeout

``java mockServer.expect(requestTo("/payments/123")) .andRespond(withSuccess().body("{\"id\":\"123\"}").throttle(10000)); // 10 sec delay ``

Important: MockRestServiceServer works with RestClientโ€™s ClientHttpRequestFactory. If you use a custom factory, you may need to bind differently. See the docs for advanced scenarios.

PaymentServiceTest.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.test.web.client.MockRestServiceServer;
import org.springframework.web.client.RestClient;

import static org.springframework.test.web.client.match.MockRestRequestMatchers.method;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
import static org.junit.jupiter.api.Assertions.*;

class PaymentServiceTest {

    private MockRestServiceServer mockServer;
    private PaymentService paymentService;

    @BeforeEach
    void setUp() {
        RestClient restClient = RestClient.builder()
                .baseUrl("https://api.example.com")
                .build();
        mockServer = MockRestServiceServer.bindTo(restClient).build();
        paymentService = new PaymentService(restClient);
    }

    @Test
    void shouldReturnPayment() {
        String json = "{\"id\":\"123\",\"amount\":100.0}";
        mockServer.expect(requestTo("/payments/123"))
                .andExpect(method(HttpMethod.GET))
                .andRespond(withSuccess(json, MediaType.APPLICATION_JSON));

        Payment payment = paymentService.getPayment("123");

        assertEquals("123", payment.id());
        assertEquals(100.0, payment.amount());
    }
}
๐Ÿ’กMockRestServiceServer Works with RestClient Too
๐Ÿ“Š Production Insight
Iโ€™ve seen teams skip testing HTTP clients because they thought it required spinning up a real server. MockRestServiceServer is lightweight and catches integration issues early.
๐ŸŽฏ Key Takeaway
Use MockRestServiceServer to test RestClient interactions without hitting real endpoints. It integrates seamlessly.

What the Official Docs Won't Tell You

After using RestClient in production for over a year, here are the gotchas that the Spring docs gloss over.

1. RestClient.create() Uses JDK HttpClient with No Timeouts

RestClient.create() creates a client using JdkClientHttpRequestFactory (Java 11+). This factory has no default timeouts. In production, a downstream slow service will hang your threads indefinitely. Always use a custom factory with explicit timeouts.

2. onStatus Handlers Must Throw or Handle

If your onStatus handler doesn't throw an exception and doesn't consume the response body, RestClient will return null from body() or toEntity(). This leads to confusing NullPointerException. The official docs mention this, but itโ€™s easy to miss. My rule: always throw a custom exception in onStatus.

3. MockRestServiceServer with Custom Request Factory

If you configure a custom ClientHttpRequestFactory (e.g., Apache HttpClient), MockRestServiceServer.bindTo(restClient) might not work because it uses the default SimpleClientHttpRequestFactory. You need to bind to the underlying ClientHttpRequestFactory or use bindTo(restClient).ignoreExpectOrder(true). See the test section for workarounds.

4. Exchange vs Retrieve: Memory Leak Risk

When using exchange(), you must close the response body. If you don't, the underlying HTTP connection may not be released. The retrieve() method handles this automatically. Only use exchange when you need low-level control, and always close the body in a finally block.

5. Default Headers Are Not Merged with Request Headers

If you set a default header (e.g., Authorization) in the builder, and then set the same header on a specific request, the request header overrides the default. This is consistent with HTTP semantics, but it surprises many developers. If you want to merge, you need to do it manually.

6. RestClient Is Not a Drop-in Replacement for RestTemplate

While you can wrap a RestTemplate with RestClient.builder(restTemplate), the resulting RestClient inherits RestTemplate's quirks, like its exception hierarchy. For a clean migration, build a new RestClient from scratch.

RestClientGotchas.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Gotcha 1: No timeouts with create()
RestClient badClient = RestClient.create(); // No timeouts!

// Gotcha 2: onStatus must throw
restClient.get()
    .uri("/test")
    .retrieve()
    .onStatus(HttpStatusCode::is4xxClientError, (req, resp) -> {
        // This doesn't throw -> body() returns null
    })
    .body(String.class); // Might return null

// Gotcha 5: Default header override
RestClient client = RestClient.builder()
    .defaultHeader("Authorization", "Bearer default")
    .build();
client.get()
    .uri("/test")
    .header("Authorization", "Bearer override") // overrides default
    .retrieve();
โš  Don't Trust the Defaults
๐Ÿ“Š Production Insight
In one incident, a team used RestClient.create() and wondered why their service hung during a downstream outage. The fix: configure timeouts. The lesson: never trust defaults in production.
๐ŸŽฏ Key Takeaway
Be aware of RestClient's pitfalls: no default timeouts, onStatus must throw, and exchange body management. These are the most common sources of bugs.

Migrating from RestTemplate to RestClient

If you have an existing codebase with RestTemplate, you don't need to rewrite everything overnight. Hereโ€™s a pragmatic migration strategy.

Step 1: Wrap RestTemplate

You can create a RestClient that delegates to an existing RestTemplate:

``java RestTemplate restTemplate = new RestTemplate(); RestClient restClient = RestClient.builder(restTemplate).build(); ``

This gives you the fluent API while keeping the old RestTemplate configuration. But this is a stepping stoneโ€”you still have RestTemplate under the hood.

Step 2: Replace Calls Incrementally

Start with the most critical or simplest endpoints. Replace restTemplate.getForObject() with restClient.get().retrieve().body(). Keep the old code until youโ€™ve tested the new one.

Step 3: Migrate Error Handling

RestTemplate throws RestClientException subclasses like HttpClientErrorException. RestClient uses onStatus. You can create a compatibility layer:

``java restClient.get() .uri(url) .retrieve() .onStatus(HttpStatusCode::is4xxClientError, (request, response) -> { throw new HttpClientErrorException(response.getStatusCode(), response.getStatusText()); }) .body(String.class); ``

Step 4: Remove RestTemplate Eventually

Once all calls are migrated, remove the RestTemplate bean. This may break other code that depends on it, so do it in a separate release.

Common Pitfalls

  • URI Template Variables: RestTemplate uses UriTemplateHandler, RestClient uses uri(String, Object...). They are compatible, but check edge cases with encoded characters.
  • Exchange vs retrieve: restTemplate.exchange() becomes restClient.get().retrieve().toEntity(). For full control, use exchange().
  • Interceptors: RestTemplateโ€™s ClientHttpRequestInterceptor works with RestClient. You can reuse them.

When Not to Migrate

If your app is short-lived or RestTemplate usage is minimal, migration may not be worth it. But for long-term projects, RestClient is the future.

MigrationExample.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Before: RestTemplate
RestTemplate restTemplate = new RestTemplate();
String result = restTemplate.getForObject("https://api.example.com/data", String.class);

// After: RestClient (wrapping existing RestTemplate)
RestClient restClient = RestClient.builder(restTemplate).build();
String result = restClient.get()
        .uri("https://api.example.com/data")
        .retrieve()
        .body(String.class);

// Eventually: new RestClient without RestTemplate
RestClient newClient = RestClient.builder()
        .baseUrl("https://api.example.com")
        .build();
String result = newClient.get()
        .uri("/data")
        .retrieve()
        .body(String.class);
๐Ÿ”ฅMigration Is a Journey, Not a Sprint
๐Ÿ“Š Production Insight
I advised a client to do a gradual migration over three sprints. They tried to do it in one and broke several endpoints. Slow and steady wins the race.
๐ŸŽฏ Key Takeaway
Migrate from RestTemplate to RestClient incrementally by wrapping the existing RestTemplate, then replacing calls one by one.
● Production incidentPOST-MORTEMseverity: high

The 2 AM RestTemplate Timeout Outage

Symptom
Payment processing hung for 30+ seconds, causing downstream services to timeout and users to see '500 Internal Server Error'.
Assumption
The developer assumed RestTemplate had sensible default timeouts (like 5 seconds).
Root cause
RestTemplate (using the default SimpleClientHttpRequestFactory) has NO timeout by default. A slow downstream service caused threads to block indefinitely, exhausting the connection pool.
Fix
Switched to RestClient with explicit timeouts and a circuit breaker (Resilience4j).
Key lesson
  • Always set explicit timeouts on HTTP clients in production.
  • Use RestClient's builder to configure timeouts, or wrap with a custom ClientHttpRequestFactory.
  • Monitor thread pool exhaustionโ€”itโ€™s a silent killer.
  • RestTemplateโ€™s default error handling masks real issues; RestClient gives you clear status handlers.
  • Migrate legacy RestTemplate code to RestClient proactively.
Production debug guideSymptom to Action4 entries
Symptom · 01
RestClient call hangs indefinitely
Fix
Check connect/read timeouts. Use builder.connectTimeout(Duration.ofSeconds(5)).readTimeout(Duration.ofSeconds(10)).
Symptom · 02
Unexpected 4xx/5xx response not caught
Fix
Add .defaultStatusHandler(HttpStatusCode::isError, (req, resp) -> { throw new MyCustomException(resp.getStatusCode()); })
Symptom · 03
Connection pool exhaustion
Fix
Verify HttpClient configuration. Use a pooled connection manager (e.g., PoolingHttpClientConnectionManager) and set max connections.
Symptom · 04
RestClient throws UnknownContentTypeException
Fix
Check the Accept header and the response's Content-Type. Register appropriate HttpMessageConverters.
★ Quick Debug Cheat SheetFast fixes for common RestClient issues in production.
Timeout
Immediate action
Set connectTimeout and readTimeout in builder.
Commands
RestClient.builder().connectTimeout(Duration.ofSeconds(5)).build()
RestClient.builder().requestFactory(new HttpComponentsClientHttpRequestFactory()).build()
Fix now
Add timeouts and switch to Apache HttpClient for better control.
Error handling not triggered+
Immediate action
Add defaultStatusHandler for 4xx/5xx.
Commands
RestClient.builder().defaultStatusHandler(HttpStatusCode::isError, (req, resp) -> {}).build()
Use onStatus for specific status codes.
Fix now
Implement custom error handling to avoid silent failures.
SSL handshake failure+
Immediate action
Check certificate trust store. Use a custom SSLContext.
Commands
new HttpComponentsClientHttpRequestFactory() with SSLContext
RestClient.builder().requestFactory(factory).build()
Fix now
Ensure the correct certificate is in the trust store.
FeatureRestTemplateRestClient
API StyleImperative, callback-heavyFluent, declarative
ImmutabilityMutable (can change settings after creation)Immutable (thread-safe once built)
Error HandlingThrows exceptions for all 4xx/5xxDeclarative onStatus handlers
TestabilityMockRestServiceServerMockRestServiceServer (same)
Async SupportNo (use AsyncRestTemplate, deprecated)No (use WebClient for async)
IntroducedSpring 3.0Spring 6.1 (Boot 3.2)
⚙ Quick Reference
7 commands from this guide
FileCommand / CodePurpose
RestClientBasicExample.javapublic class RestClientBasicExample {Why RestClient? The End of RestTemplate
RestClientConfig.java@ConfigurationBuilding and Configuring RestClient
RestClientRequests.javapublic class RestClientRequests {Making Requests
RestClientErrorHandling.javapublic class RestClientErrorHandling {Error Handling and Status Handlers
PaymentServiceTest.javaclass PaymentServiceTest {Testing RestClient with MockRestServiceServer
RestClientGotchas.javaRestClient badClient = RestClient.create(); // No timeouts!What the Official Docs Won't Tell You
MigrationExample.javaRestTemplate restTemplate = new RestTemplate();Migrating from RestTemplate to RestClient

Key takeaways

1
RestClient is the modern, fluent replacement for RestTemplate, offering better error handling and immutability.
2
Always configure timeouts, connection pooling, and error handlers for production use.
3
Test RestClient with MockRestServiceServer to avoid hitting real endpoints in unit tests.
4
Migrate from RestTemplate incrementally by wrapping it first, then replacing calls.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
Explain how RestClient handles HTTP status codes differently from RestTe...
Q02SENIOR
How would you configure RestClient for production with timeouts and conn...
Q03SENIOR
What is the difference between `retrieve()` and `exchange()` in RestClie...
Q01 of 03SENIOR

Explain how RestClient handles HTTP status codes differently from RestTemplate.

ANSWER
RestClient provides a declarative onStatus method that allows you to handle specific status codes or ranges with lambda expressions. RestTemplate throws exceptions for all 4xx/5xx responses, requiring try-catch blocks. RestClient's approach is more readable and testable.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
Is RestClient thread-safe?
02
Can I use RestClient with WebClient?
03
How do I handle file uploads with RestClient?
04
Does RestClient support HTTP/2?
05
What is the difference between RestClient and WebClient?
N
Naren Founder & Principal Engineer

20+ years shipping production Java in banking & fintech. Notes here come from systems that actually shipped.

Follow
Verified
production tested
July 15, 2026
last updated
2,398
articles · all by Naren
🔥

That's Spring Boot. Mark it forged?

6 min read · try the examples if you haven't

Previous
The Complete Guide to RestTemplate in Spring: GET, POST, PUT, DELETE
102 / 121 · Spring Boot
Next
Uploading Files with Spring RestTemplate: MultipartFile and Streaming