What happens by default when a Spring REST client (RestTemplate or RestClient) receives a 4xx or 5xx HTTP response?
answer
- DefaultResponseErrorHandler: 4xx/5xx = error
- 4xx -> HttpClientErrorException, 5xx -> HttpServerErrorException
- both extend RestClientResponseException
- ResourceAccessException = I/O, no response
- all unchecked RuntimeExceptions
basics
~10 sBy 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 sSpring'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 linesRestClient 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
Must know that 4xx/5xx throw by default and the two exception names.
Should name RestClientResponseException as the common parent and know where the body lives.
Distinguishes retrieve() default handling from exchange() (no default handling) and ResourceAccessException from status exceptions.
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.