Given an HttpClientErrorException or HttpServerErrorException, how do you extract the status, headers, and deserialize the error response body?
answer
- common parent = RestClientResponseException
- getStatusCode / getResponseHeaders / getResponseBodyAsString
- getResponseBodyAs(Class) deserializes via converters
- subclasses: NotFound, TooManyRequests, ServiceUnavailable...
- catch subclass before parent (unreachable-code rule)
basics
~10 sThese extend RestClientResponseException, which gives getStatusCode(), getResponseHeaders(), getResponseBodyAsString(), and getResponseBodyAs(Class) to deserialize the error payload into a POJO. getStatusText() gives the reason phrase.
solid answer
~30 sBoth HttpClientErrorException (4xx) and HttpServerErrorException (5xx) extend HttpStatusCodeException, which extends RestClientResponseException. That parent buffers the whole error response, so you can call getStatusCode() (an HttpStatusCode), getStatusText(), getResponseHeaders() (HttpHeaders), getResponseBodyAsString(charset) / getResponseBodyAsByteArray(), and — most usefully — getResponseBodyAs(Class) or getResponseBodyAs(ParameterizedTypeReference) to deserialize a structured error (like an RFC 7807 ProblemDetail or a custom {code,message} body) using the client's configured message converters. You can also catch specific subclasses like HttpClientErrorException.NotFound or HttpClientErrorException.TooManyRequests for targeted handling, since the factory create() methods return those subtypes. For borderline behavior always catch the broadest type you need (RestClientResponseException) and branch on getStatusCode() when subclass granularity isn't enough.
code
java · 15 linestry {
return restClient.get().uri("/orders/{id}", id)
.retrieve()
.body(Order.class);
} catch (HttpClientErrorException.TooManyRequests e) { // specific 429
String retryAfter = e.getResponseHeaders().getFirst("Retry-After");
throw new RateLimitedException(retryAfter);
} catch (HttpClientErrorException e) { // other 4xx
// deserialize a structured error body (e.g. RFC 7807)
ProblemDetail problem = e.getResponseBodyAs(ProblemDetail.class);
throw new OrderClientException(e.getStatusCode(), problem);
} catch (RestClientResponseException e) { // 5xx + unknown codes
log.warn("downstream error {} body={}", e.getStatusCode(), e.getResponseBodyAsString());
throw e;
}go deeper
Can catch the exception and read getResponseBodyAsString().
Knows getStatusCode/headers and that a common parent exists.
Deserializes structured error bodies with getResponseBodyAs and catches specific subclasses; reads Retry-After.
Designs a shared error-contract mapping (ProblemDetail) so all integrations expose consistent, typed failures.
## Why the body is available at all When the default handler throws, it doesn't discard the response — it buffers status, headers, and body into the exception. The buffering lives on **`RestClientResponseException`**, the common ancestor of `HttpClientErrorException`, `HttpServerErrorException`, and `UnknownHttpStatusCodeException`. So *whatever* status-bearing exception you catch, the same accessors work. ## The accessors - `getStatusCode()` → `HttpStatusCode` (use `.value()` for the int, or compare with `HttpStatus.NOT_FOUND`). - `getStatusText()` → the HTTP reason phrase. - `getResponseHeaders()` → `HttpHeaders` (e.g., read `Retry-After` on a 429/503). - `getResponseBodyAsString()` / `getResponseBodyAsString(Charset)` → raw text body. - `getResponseBodyAsByteArray()` → raw bytes. - `getResponseBodyAs(Class<T>)` and `getResponseBodyAs(ParameterizedTypeReference<T>)` → **deserialize** the error body via the same `HttpMessageConverter`s the client uses (e.g., Jackson). Great for structured error contracts. ## Structured error bodies (RFC 7807) Spring models Problem Details as `ProblemDetail`. If a service returns `application/problem+json`, you can do `ex.getResponseBodyAs(ProblemDetail.class)` to read `type`, `title`, `status`, `detail`, `instance`, and extension fields. ## Catching specific subclasses The `create(...)` factories map well-known codes to concrete subclasses so you can be surgical: - 4xx: `HttpClientErrorException.BadRequest`, `.Unauthorized`, `.Forbidden`, `.NotFound`, `.Conflict`, `.UnprocessableEntity`, `.TooManyRequests`, ... - 5xx: `HttpServerErrorException.InternalServerError`, `.BadGateway`, `.ServiceUnavailable`, `.GatewayTimeout`, ... Catch these directly, or catch `HttpClientErrorException` / `HttpServerErrorException` and switch on the status, or catch `RestClientResponseException` to cover both series in one block. ## Rate-limit / retry hints On `429 Too Many Requests` or `503 Service Unavailable`, read `getResponseHeaders().getFirst("Retry-After")` to drive backoff. ## Gotchas - `getResponseBodyAs(...)` needs a converter capable of the response's content type; if the error body is HTML but you ask for a JSON POJO, it fails. Guard with content-type or fall back to `getResponseBodyAsString()`. - The body is buffered once; you don't re-read a stream — these accessors are safe to call repeatedly. - Subclass catch blocks must precede the parent (`NotFound` before `HttpClientErrorException`) or the compiler flags unreachable code. - `UnknownHttpStatusCodeException` is a sibling, not under Http*ErrorException — catch `RestClientResponseException` if you must cover truly unknown codes too.
- How would you deserialize an RFC 7807 problem+json error body from the exception?Call ex.getResponseBodyAs(ProblemDetail.class); it uses the client's message converters to map the buffered body into a ProblemDetail with title/status/detail and extensions.
- Where does UnknownHttpStatusCodeException sit relative to HttpClientErrorException?It's a sibling under RestClientResponseException, not under HttpStatusCodeException/Http*ErrorException. To cover unknown codes too, catch RestClientResponseException.
saying these in an interview costs you the question
- Thinking the error body is lost once the exception is thrown — it's buffered on the exception.
- Catching HttpClientErrorException before its subclass (unreachable code) or expecting subclasses under the wrong parent.
- Assuming getResponseBodyAs works regardless of content type — it needs a matching converter.