skip to content

Synchronous HTTP Clients

Calling other services from a blocking application: the modern RestClient, the legacy RestTemplate, declarative @HttpExchange interfaces, and how client errors surface. Interviewers ask because service-to-service calls are where timeouts and retries get forgotten.

part ofSpring Frameworkoverview, primer and where to startread it →
on this pageshow

explore

questions

20

What happens by default when a Spring REST client (RestTemplate or RestClient) receives a 4xx or 5xx HTTP response?

level: juniorimportance: must knowfreq 78%

answer

  1. DefaultResponseErrorHandler: 4xx/5xx = error
  2. 4xx -> HttpClientErrorException, 5xx -> HttpServerErrorException
  3. both extend RestClientResponseException
  4. ResourceAccessException = I/O, no response
  5. all unchecked RuntimeExceptions

basics

~10 s

By default the client treats 4xx/5xx as errors and throws an exception: HttpClientErrorException for 4xx and HttpServerErrorException for 5xx. A successful 2xx just returns the body normally.

solid answer

~30 s

Spring's REST clients ship with a DefaultResponseErrorHandler that classifies any 4xx or 5xx status as an error. On RestTemplate calls and on RestClient's retrieve() path, that handler throws automatically: HttpClientErrorException for 4xx (400/404/etc.), HttpServerErrorException for 5xx (500/503/etc.), and UnknownHttpStatusCodeException for non-standard codes. All three extend RestClientResponseException, which exposes the status code, response headers, and the raw error body. A network/IO failure (connection refused, read timeout) is different — that surfaces as ResourceAccessException, not a status exception, because there was no HTTP response at all. So by default you don't check status codes manually; you catch exceptions.

code

java · 17 lines
java
RestClient client = RestClient.create();

try {
    // .retrieve() applies default status handling: 4xx/5xx throw here
    User user = client.get()
            .uri("https://api.example.com/users/{id}", 42)
            .retrieve()
            .body(User.class);
} catch (HttpClientErrorException e) {      // 4xx
    // e.g. 404 -> user not found
    HttpStatusCode status = e.getStatusCode();
    String errorBody = e.getResponseBodyAsString();
} catch (HttpServerErrorException e) {      // 5xx
    // downstream is broken; maybe retry or fail fast
} catch (ResourceAccessException e) {       // no HTTP response at all (timeout/connection refused)
    // network problem
}

go deeper

for a junior

Must know that 4xx/5xx throw by default and the two exception names.

for a middle

Should name RestClientResponseException as the common parent and know where the body lives.

for a senior

Distinguishes retrieve() default handling from exchange() (no default handling) and ResourceAccessException from status exceptions.

for a principal

Frames default behavior as a policy that must be intentionally shaped for resilience across many downstream calls.

## The default contract Spring's synchronous HTTP clients — the older `RestTemplate` and the newer fluent `RestClient` — both wire in a `DefaultResponseErrorHandler` out of the box. This class implements the `ResponseErrorHandler` interface, which decides two things: (1) *is this response an error?* via `hasError(...)`, and (2) *what do we do about it?* via `handleError(...)`. The default `hasError` returns `true` for any status in the **4xx (client error)** or **5xx (server error)** series. Everything else (1xx, 2xx, 3xx) is treated as non-error. ## What gets thrown When a response is an error, the default handler throws a subtype of `RestClientResponseException`: - **`HttpClientErrorException`** for **4xx** statuses (e.g. 400 Bad Request, 401 Unauthorized, 404 Not Found). - **`HttpServerErrorException`** for **5xx** statuses (e.g. 500 Internal Server Error, 503 Service Unavailable). - **`UnknownHttpStatusCodeException`** when the numeric status doesn't map to a known series/value. All of these carry the response so you can inspect it: `getStatusCode()`, `getStatusText()`, `getResponseHeaders()`, `getResponseBodyAsString()` / `getResponseBodyAsByteArray()`, and `getResponseBodyAs(Class)` to deserialize the error payload. ## The full exception hierarchy ``` RestClientException (root, unchecked) ├─ ResourceAccessException (I/O: no HTTP response) └─ RestClientResponseException (got a response w/ a status) ├─ UnknownHttpStatusCodeException └─ HttpStatusCodeException ├─ HttpClientErrorException (4xx) → NotFound, BadRequest, ... └─ HttpServerErrorException (5xx) → InternalServerError, ... ``` Key distinction for juniors: a **status exception** means the server answered but with an error status; a **`ResourceAccessException`** means the request never completed at the HTTP level (DNS failure, connection refused, socket/read timeout). All are unchecked (`RuntimeException`), so the compiler won't force you to handle them. ## Where the difference matters between the two clients - **RestTemplate**: every method (`getForObject`, `exchange`, ...) runs the error handler, so 4xx/5xx always throw unless you replace the handler. - **RestClient `.retrieve()`**: applies the default status handling, so `body(...)`/`toEntity(...)` throw on 4xx/5xx. - **RestClient `.exchange(...)`**: the escape hatch — it does **not** apply the default status handlers; you get the raw `ClientHttpResponse` and decide everything yourself. ## Gotchas - Redirects (3xx) are *not* errors here; the underlying request factory usually follows them. - The error body is only readable once and is buffered by the exception — read it from the exception, not by re-reading the stream. - Because these are unchecked, an unhandled 500 from a downstream call will bubble up and (in a controller) typically become your own 500 unless you translate it.

  • What exception do you get for a connection timeout versus a 500 response?
    A connection/read timeout (no HTTP response) surfaces as ResourceAccessException; a 500 response surfaces as HttpServerErrorException (a RestClientResponseException carrying the status and body).
  • Are these exceptions checked or unchecked?
    Unchecked — they all extend RestClientException which extends RuntimeException, so the compiler doesn't force a try/catch.

saying these in an interview costs you the question

  • Claiming the client returns null or an empty body on 4xx/5xx instead of throwing.
  • Thinking you must manually check response.getStatusCode() with RestTemplate — the default handler already throws.
  • Confusing a timeout (ResourceAccessException) with a 5xx status exception.

context

open as a page

What is a Spring HTTP Interface (declarative HTTP client), and how do you declare one with @HttpExchange?

level: juniorimportance: must knowfreq 55%

basics

~10 s

You define a Java interface with methods annotated like @GetExchange("/users/{id}"). Spring generates a proxy that turns each call into a real HTTP request, so you never write HTTP plumbing by hand.

open as a page

What is Spring's RestClient and how does its fluent API differ from the older RestTemplate?

level: juniorimportance: must knowfreq 70%

basics

~10 s

RestClient is a synchronous HTTP client (Spring 6.1+) with a modern fluent, chained API — client.get().uri(...).retrieve().body(Type.class). RestTemplate is the older template-method client it's meant to replace for blocking calls.

open as a page

What is Spring's RestTemplate, and how do you make a simple GET and POST call with it?

level: juniorimportance: must knowfreq 70%

basics

~10 s

RestTemplate is Spring's synchronous HTTP client for calling REST APIs. Use getForObject(url, Type.class) to GET and deserialize a response, and postForObject/postForEntity(url, body, Type.class) to POST a body and read the reply.

open as a page

How do you customize error handling for specific status codes with RestClient's onStatus handlers?

level: middleimportance: must knowfreq 70%

basics

~20 s

On the retrieve() path, chain .onStatus(predicate, handler). The predicate tests the HttpStatusCode; the handler receives the request and ClientHttpResponse so you can throw your own exception or read the body. Matching handlers replace the default throw.

open as a page

How do you turn an @HttpExchange interface into a usable client using HttpServiceProxyFactory and an adapter?

level: middleimportance: must knowfreq 50%

basics

~10 s

Wrap a configured RestClient (or WebClient) in an adapter, pass it to HttpServiceProxyFactory.builderFor(adapter).build(), then call factory.createClient(MyInterface.class) to get a proxy you register as a bean.

open as a page

How do you send a POST request with a JSON body and custom headers using RestClient, and how is the request body serialized?

level: middleimportance: must knowfreq 60%

basics

~10 s

Use restClient.post().uri(...).contentType(MediaType.APPLICATION_JSON).header(...).body(payloadObject).retrieve().body(ResponseType.class). The payload object is serialized to JSON by an HttpMessageConverter (Jackson).

open as a page

What is the difference between retrieve() and exchange() on RestClient, and how does error/status handling differ between them?

level: seniorimportance: must knowfreq 55%

basics

~10 s

retrieve() is the high-level path that auto-throws on 4xx/5xx and lets you extract body/entity; exchange() gives you the raw request+response to inspect status/headers/body yourself and, by default, does NOT throw on error status.

open as a page

Is RestTemplate deprecated? How does it compare to RestClient and WebClient, and what should new code use?

level: seniorimportance: must knowfreq 65%

basics

~20 s

RestTemplate is not deprecated but is in maintenance mode (bugfixes only) since Spring 5. For new synchronous code prefer RestClient (Spring 6.1+), which has a modern fluent API on the same infrastructure. Use WebClient for reactive/non-blocking code.

open as a page

When and how do you use RestTemplate.exchange(), and how do you send custom request headers?

level: middleimportance: should knowfreq 55%

basics

~20 s

Use exchange() when the simple verb methods aren't enough — e.g. to set custom headers or read a generic type. You wrap the body and headers in an HttpEntity, pass an HttpMethod, and get back a ResponseEntity.

open as a page

Given an HttpClientErrorException or HttpServerErrorException, how do you extract the status, headers, and deserialize the error response body?

level: seniorimportance: should knowfreq 50%

basics

~10 s

These extend RestClientResponseException, which gives getStatusCode(), getResponseHeaders(), getResponseBodyAsString(), and getResponseBodyAs(Class) to deserialize the error payload into a POJO. getStatusText() gives the reason phrase.

open as a page

What is the ResponseErrorHandler interface and how do you plug a custom one into RestTemplate or RestClient?

level: seniorimportance: should knowfreq 55%

basics

~10 s

ResponseErrorHandler is the strategy Spring uses to decide if a response is an error and handle it. It has hasError(response) and handleError(response). Set a custom one via RestTemplate.setErrorHandler(...) or RestClient.Builder.defaultStatusHandler(...).

open as a page

How does error handling work for HTTP Interface calls, and how would you unit/integration test a declarative client?

level: seniorimportance: should knowfreq 32%

basics

~10 s

Errors surface as exceptions from the underlying client (e.g. RestClient throws HttpClientErrorException/HttpServerErrorException on 4xx/5xx). Customize via the client's status handlers. Test by pointing the client at a MockWebServer or mocking the interface directly.

open as a page

When do you back an HTTP Interface with the RestClientAdapter versus the WebClientAdapter, and how does that affect return types?

level: seniorimportance: should knowfreq 40%

basics

~10 s

Use RestClientAdapter for blocking/servlet apps — methods return plain bodies or ResponseEntity. Use WebClientAdapter in reactive apps to additionally return Mono/Flux. RestTemplateAdapter exists for legacy code.

open as a page

How do you configure the underlying transport (connection/read timeouts, connection pooling) for RestClient via ClientHttpRequestFactory?

level: seniorimportance: should knowfreq 45%

basics

~10 s

Set a ClientHttpRequestFactory on the builder: RestClient.builder().requestFactory(factory).build(). Use e.g. JdkClientHttpRequestFactory, HttpComponentsClientHttpRequestFactory, or SimpleClientHttpRequestFactory, and configure connect/read timeouts (and pooling) on that factory.

open as a page

How do HttpMessageConverters and ClientHttpRequestInterceptors work in RestTemplate, and what are they used for?

level: seniorimportance: should knowfreq 45%

basics

~10 s

Message converters serialize/deserialize request and response bodies (Jackson for JSON). Interceptors wrap every request/response so you can add cross-cutting behavior like auth headers, logging, or correlation IDs without touching each call site.

open as a page

How would you design a robust, cross-cutting error-handling strategy for many downstream RestClient calls, and how does exchange() fit in?

level: principalimportance: should knowfreq 38%

basics

~20 s

Centralize policy on the client builder (defaultStatusHandler) to translate 4xx/5xx into a small set of domain exceptions with the body attached; distinguish those from ResourceAccessException (I/O). Use exchange() where you need full manual status control, and combine with retries/circuit breakers.

open as a page

In a Spring MVC (servlet) application, when would you choose RestClient over WebClient, and what are the concurrency/thread implications?

level: principalimportance: should knowfreq 35%

basics

~10 s

In a blocking servlet app, prefer RestClient: it's synchronous, needs no reactive stack, and matches the thread-per-request model. Use WebClient only when you genuinely need non-blocking/async/streaming or high fan-out concurrency.

open as a page

How do you make RestTemplate production-ready regarding timeouts, connection pooling, and thread-safety?

level: principalimportance: should knowfreq 40%

basics

~10 s

Always set connect and read timeouts, back it with a pooled ClientHttpRequestFactory (e.g. Apache HttpClient) instead of the default one-connection-per-call factory, and reuse one configured, thread-safe RestTemplate as a singleton.

open as a page

At scale, how do you register many HTTP Interface clients cleanly, share configuration, and when would you NOT use declarative HTTP interfaces?

level: principalimportance: nice to knowfreq 22%

basics

~20 s

Share a pre-configured RestClient.Builder and register each interface as an @Bean via one factory (or use Spring's group registration). Skip declarative interfaces when calls are highly dynamic, need per-call streaming/low-level control, or when one-off imperative RestClient calls are simpler.

open as a page