skip to content

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

level: seniorimportance: should knowfreq 55%

answer

  1. ResponseErrorHandler = hasError + handleError
  2. DefaultResponseErrorHandler flags 4xx/5xx
  3. RestTemplate.setErrorHandler / RestClient defaultStatusHandler
  4. handleError returns w/o throwing = swallow
  5. extend Default... to tweak one status

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(...).

solid answer

~40 s

ResponseErrorHandler is the SPI that both RestTemplate and RestClient use for error decisions. Its core methods are hasError(ClientHttpResponse) — is this an error? — and handleError(...) — react to it, usually by throwing. DefaultResponseErrorHandler is the built-in that flags 4xx/5xx and throws the HttpClientErrorException/HttpServerErrorException family. To override globally on RestTemplate you call setErrorHandler(myHandler), replacing the default entirely; on RestClient you use builder().defaultStatusHandler(...). A common override is extending DefaultResponseErrorHandler and customizing hasError or handleError — for example, treating 404 as a non-error so callers get an empty body, or translating downstream statuses into your own domain exceptions with the response body attached. Prefer per-request onStatus for narrow cases and a ResponseErrorHandler for whole-client policy. Note handleError should throw or return cleanly; if it returns without throwing, processing continues.

code

java · 25 lines
java
// Treat 404 as "not an error" and translate other errors to a domain exception.
class LenientErrorHandler extends DefaultResponseErrorHandler {
    @Override
    public boolean hasError(ClientHttpResponse response) throws IOException {
        if (response.getStatusCode().value() == 404) {
            return false; // caller gets an empty/absent body instead of an exception
        }
        return super.hasError(response);
    }

    @Override
    public void handleError(ClientHttpResponse response) throws IOException {
        String body = StreamUtils.copyToString(response.getBody(), StandardCharsets.UTF_8);
        throw new CatalogIntegrationException(response.getStatusCode(), body);
    }
}

// RestTemplate: replaces the default handler
RestTemplate template = new RestTemplate();
template.setErrorHandler(new LenientErrorHandler());

// RestClient: whole-client policy
RestClient client = RestClient.builder()
        .defaultStatusHandler(new LenientErrorHandler())
        .build();

go deeper

for a junior

May not know the interface exists; knows only that errors throw.

for a middle

Knows DefaultResponseErrorHandler is the default and that onStatus customizes.

for a senior

Can implement/extend ResponseErrorHandler for client-wide policy and knows the swallow semantics.

for a principal

Uses a shared error-handler component for uniform exception translation, logging, and metrics across integrations, and reasons about failure-hiding risks.

## The interface `org.springframework.web.client.ResponseErrorHandler` is the strategy interface both clients delegate to. Its methods: - `boolean hasError(ClientHttpResponse response)` — classify: is this an error worth handling? - `void handleError(ClientHttpResponse response)` and the richer overload `handleError(URI url, HttpMethod method, ClientHttpResponse response)` — act on an error, typically by throwing. The client calls `hasError`; if it returns `true`, it calls `handleError`. If `handleError` **returns without throwing**, the client proceeds to read the body as if successful — which is how you deliberately *swallow* a status. ## The default implementation `DefaultResponseErrorHandler` implements this: `hasError` returns true for 4xx/5xx; `handleError` inspects the series and throws `HttpClientErrorException` (4xx), `HttpServerErrorException` (5xx), or `UnknownHttpStatusCodeException`, using `HttpClientErrorException.create(...)` / `HttpServerErrorException.create(...)` factory methods that yield status-specific subclasses (e.g. `HttpClientErrorException.NotFound`). It buffers the body into the exception so callers can read it later. ## Plugging in a custom handler - **RestTemplate**: `restTemplate.setErrorHandler(new MyErrorHandler())`. This *replaces* the default for that template. - **RestClient**: `RestClient.builder().defaultStatusHandler(ResponseErrorHandler)` or `defaultStatusHandler(Predicate<HttpStatusCode>, ErrorHandler)`. Applies to every request from that client. ## Two idiomatic override patterns 1. **Extend and tweak**: subclass `DefaultResponseErrorHandler`, override `hasError` (e.g., return false for 404 so it's not an error) or override `handleError` to translate exceptions. 2. **Full replacement**: implement `ResponseErrorHandler` directly for total control (rare; you lose the sensible default body-buffering unless you reimplement it). ## Reading the body safely Inside `handleError` you have a `ClientHttpResponse`: `getStatusCode()`, `getHeaders()`, `getBody()` (InputStream). Because the stream is consumable once, capture it immediately (e.g., `StreamUtils.copyToString`) before throwing so the message/exception carries it. ## When to use which - **onStatus / per-request**: narrow, endpoint-specific behavior. - **ResponseErrorHandler / defaultStatusHandler**: cross-cutting policy — uniform exception translation, metrics, correlation-id logging — for the whole client. ## Gotchas - Replacing the handler on RestTemplate removes default 4xx/5xx throwing entirely; if your `hasError` returns false everywhere, callers never see errors — easy way to accidentally hide failures. - `handleError` returning normally = swallow. Make sure you *intend* that. - Reading `getBody()` twice fails; buffer once. - ResourceAccessException (I/O) never reaches ResponseErrorHandler — there's no ClientHttpResponse for a failed connection.

  • What happens if your handleError implementation returns without throwing?
    The client treats the response as handled and continues to read the body as if successful — effectively swallowing the error. This is the mechanism for turning, say, a 404 into an empty result.
  • Does ResponseErrorHandler ever see a connection timeout?
    No. A timeout/connection-refused produces no ClientHttpResponse, so it never reaches the handler — it becomes a ResourceAccessException from the request factory instead.

saying these in an interview costs you the question

  • Saying setErrorHandler augments the default — it replaces it, so you can accidentally disable all error throwing.
  • Forgetting that a handleError that returns normally swallows the error.
  • Expecting network/IO failures to flow through ResponseErrorHandler.

context