skip to content

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

level: seniorimportance: should knowfreq 32%

answer

  1. proxy delegates errors to the client
  2. 4xx=HttpClientErrorException, 5xx=HttpServerErrorException
  3. defaultStatusHandler to customize
  4. ResponseEntity/Optional to avoid exceptions
  5. test: mock interface OR MockWebServer

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.

solid answer

~40 s

The HTTP Interface proxy delegates transport and error behavior to the wrapped client, so error semantics come from there. With `RestClientAdapter`, a 4xx/5xx by default throws a `RestClientResponseException` subtype (`HttpClientErrorException` for 4xx, `HttpServerErrorException` for 5xx); you override this with the RestClient's `defaultStatusHandler(...)` to translate statuses into domain exceptions or swallow specific codes. `WebClientAdapter` throws `WebClientResponseException` and is customized via `onStatus(...)`/exchange filters. Returning `ResponseEntity<T>` or `Optional<T>` lets you inspect status without exceptions. Testing: for pure unit tests of your service, mock the interface (it's just an interface). For the client wiring itself, run it against an in-process fake HTTP server — okhttp `MockWebServer` or Spring's `MockRestServiceServer` (RestTemplate-backed) — asserting the outgoing request and stubbing the response.

code

java · 18 lines
java
// Wiring test with okhttp MockWebServer
MockWebServer server = new MockWebServer();
server.enqueue(new MockResponse()
        .setHeader("Content-Type", "application/json")
        .setBody("{\"id\":1,\"name\":\"Ada\"}"));
server.start();

RestClient restClient = RestClient.builder()
        .baseUrl(server.url("/").toString())
        .build();
UserClient client = HttpServiceProxyFactory
        .builderFor(RestClientAdapter.create(restClient)).build()
        .createClient(UserClient.class);

User user = client.get(1L);
assertThat(user.name()).isEqualTo("Ada");
assertThat(server.takeRequest().getPath()).isEqualTo("/users/1");
server.shutdown();

go deeper

for a junior

Know failures throw exceptions and you can mock the interface in tests.

for a middle

Name the exception types and use ResponseEntity/Optional to avoid throwing.

for a senior

Configure defaultStatusHandler and design a MockWebServer wiring test.

for a principal

Define a codebase-wide error-translation strategy, distinguish transport vs HTTP errors, and set the testing boundary between mocked-interface and fake-server tests.

## Where errors come from The proxy itself does no error policy — it forwards to the adapter's client. So error handling is whatever the **underlying** `RestClient`/`WebClient`/`RestTemplate` does: - **RestClient / RestTemplate (blocking)**: by default, a non-2xx status throws a `RestClientResponseException`: - 4xx → `HttpClientErrorException` (and subclasses like `HttpClientErrorException.NotFound`). - 5xx → `HttpServerErrorException`. - unknown status → `UnknownHttpStatusCodeException`. These carry the status code, headers, and raw body (`getResponseBodyAsString()`). - **WebClient (reactive)**: non-2xx yields an error signal of `WebClientResponseException` (with 4xx/5xx subclasses). ## Customizing error handling You configure it on the client builder, not the interface: ```java RestClient restClient = RestClient.builder() .baseUrl("https://api.example.com") .defaultStatusHandler(HttpStatusCode::is5xxServerError, (req, res) -> { throw new UpstreamUnavailableException(res.getStatusCode()); }) .defaultStatusHandler(status -> status.value() == 404, (req, res) -> { throw new UserNotFoundException(); }) .build(); ``` WebClient equivalent uses `.defaultStatusHandler(...)` / per-call `.onStatus(...)` or an `ExchangeFilterFunction`. ## Avoiding exceptions by return type Instead of exception-based flow you can declare: - `ResponseEntity<User>` — inspect `getStatusCode()` yourself. - `Optional<User>` — for endpoints where an empty result is normal. - `ResponseEntity<Void>` — care only about status/headers. ## Testing strategies **1. Unit-test the service, not the network.** Since the client is an interface, mock it directly (Mockito). Your service logic is tested without HTTP. ```java UserClient client = mock(UserClient.class); when(client.get(1L)).thenReturn(new User(1L, "Ada")); ``` **2. Test the wiring (that annotations map correctly).** Point the real proxy at a fake server: - **okhttp `MockWebServer`**: enqueue canned responses, build the RestClient with `baseUrl(server.url(...))`, call the proxy, then assert the recorded request path/method/headers/body and the deserialized result. Works for any adapter. - **`MockRestServiceServer`**: Spring's binding-and-expectation tool for `RestTemplate`; use it when the adapter is `RestTemplateAdapter`. - For `WebClient`, a `MockWebServer` or an `ExchangeFunction` stub works; `WebTestClient` targets server-side testing, not this client. ## Gotchas - Don't put try/catch expecting a `null` on 404 by default — you'll get an exception unless you return `Optional`/`ResponseEntity` or add a status handler. - Error-handling config is per-client; two interfaces sharing a `RestClient.Builder` share its handlers. - Timeouts/connection errors surface as `ResourceAccessException` (RestClient/RestTemplate), distinct from HTTP status errors — handle both. - Mocking the interface tests *your* code but not the annotation-to-request mapping; you need a fake-server test to catch a wrong path or missing `@RequestBody`.

  • By default, what does a RestClient-backed HTTP Interface do on a 404?
    It throws `HttpClientErrorException.NotFound` (a `RestClientResponseException`). To treat 404 as empty, either add a `defaultStatusHandler` for 404 or declare the method to return `Optional<T>`/`ResponseEntity<T>`.
  • How would you assert the proxy sends the correct path and body without a live server?
    Point the underlying RestClient at an okhttp MockWebServer (or use MockRestServiceServer for a RestTemplateAdapter), enqueue a stub response, invoke the method, and inspect the recorded RecordedRequest's method/path/headers/body.

saying these in an interview costs you the question

  • Assuming a failed call returns null instead of throwing.
  • Trying to configure error handling via annotation attributes.
  • Believing WebTestClient tests the outbound declarative client (it's for server-side).
  • Confusing connection/timeout errors (ResourceAccessException) with HTTP status exceptions.

context