skip to content

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