Let's face it: sometimes you're not always going to get an OK response from a downstream service. And your code needs to handle that situation.

If you're using WebClient to invoke an HTTP request, you can use the onStatus() method to handle a specific response status code. Yes, even an error code.

Full disclosure, though: I handle errors differently. And you're more than welcome to read about my preferred method of handling errors with WebClient.

But if you have a hankerin' to use onStatus(), I'll show you how to do that here.

Picking Up Where You Left Off

This guide will pick up where you left off in the guide on how to set request parameters with WebClient.

As it stands now, here's the code you've got:

List<Activity> activities = crmClient
                            .get()
                            .uri(uriBuilder -> uriBuilder
                                    .path("/activities/search")
                                    .queryParam("contactId", contactId)
                                    .queryParam("orderBy", "startDate")
                                    .queryParam("orderType", "DESC")
                                    .build())
                            .header(HttpHeaders.AUTHORIZATION, bearerToken)
                            .retrieve()
                            .bodyToFlux(Activity.class)
                            .collectList()
                            .block();

It's a service-to-service level call within an ecosystem that supports a CRM app. The code above gives you a list of a contacts actvities sorted by start date in descending order.

If you want to see the dataset that I'm working with, you can find it here.

Now let's say you want to trap a specific erroneous status code. Specifically, you want to trap status code 405.

The 405 status code ("Method Not Allowed") gets returned when you try to invoke an HTTP method against an endpoint that the downstream service recognizes, but doesn't allow for that specific method.

For example, the CRM application recognizes/activities as an endpoint because it's a URI path segment. But if you try to perform a GET against it, you'll receive a 405 response code.

In fact, that's how you'll force a 405 response code in your testing.

Run this now:

String contactId = "6014199147692f2a4194ff9a";

List<Activity> activities = crmClient
                            .get()
                            .uri(uriBuilder -> uriBuilder
                                    .path("/activities")
                                    .queryParam("contactId", contactId)
                                    .queryParam("orderBy", "startDate")
                                    .queryParam("orderType", "DESC")
                                    .build())
                            .header(HttpHeaders.AUTHORIZATION, bearerToken)
                            .retrieve()
                            .bodyToFlux(Activity.class)
                            .collectList()
                            .block();

That will get you a 405 response code.

Now that you can create the error, you can write code to handle it.

Preparing for Failure

Keep in mind: the onStatus() method lets you handle specific HTTP status responses. You aren't limited to using it for error responses only.

You can also use it to catch a range of error responses. Do that with the assistance of HttpStatus methods like is2xxSuccessful(), is4xxClientError(), etc.

But here, you'll use onStatus() to catch that pesky 405 error response. 

Here's how that's handled:

List<Activity> activities = crmClient
                            .get()
                            .uri(uriBuilder -> uriBuilder
                                    .path("/activities")
                                    .queryParam("contactId", contactId)
                                    .queryParam("orderBy", "startDate")
                                    .queryParam("orderType", "DESC")
                                    .build())
                            .header(HttpHeaders.AUTHORIZATION, bearerToken)
                            .retrieve()
                            .onStatus(status -> status.value() == HttpStatus.METHOD_NOT_ALLOWED.value(), 
                                        response -> Mono.error(new ServiceException("Method not allowed. Please check the URL.", response.statusCode().value())))
                            .bodyToFlux(Activity.class)
                            .collectList()
                            .block();

The onStatus() method takes two parameters: a Predicate and a Function. You see both of those parameters added as lambda expressions in the code above.

A Predicate is a function that returns a boolean. It's expressed here as follows:

status -> status.value() == HttpStatus.METHOD_NOT_ALLOWED.value()

The Predicate accepts HttpStatus as its input. It uses that status to determine whether it should return true or false.

In this case, the Predicate returns true if the HTTP status code equals 405. That's the value() returned by HttpStatus.METHOD_NOT_ALLOWED

If that Predicate returns true, the code invokes the Function in the second parameter. Here's how that Function is expressed:

response -> Mono.error(new ServiceException("Method not allowed. Please check the URL.", response.statusCode().value()))

The Function accepts as its input a ClientResponse object. That's the object WebClient uses to store info about the response, such as the body, the headers, and the status code.

But, thanks to type safety, the Function must return a Mono.

Here, the code instantiates the Mono object with Mono.error(). That's so it returns an error once somebody subscribes to it.

And the code should return an error because it hit a 405 response code from the downstream service.

But what kind of item should this Mono hold? It must hold an Exception.

Why? Once again, it's because of type safety. Here's the whole Function definition for your viewing pleasure:

Function<ClientResponse, Mono<? extends Throwable>> 

That means the input must be a type of ClientResponse (check) and the output must be a type of Mono (check) that contains an Exception (or Throwable) class (check, see below).

The single parameter in Mono.error() is a type of ServiceException. That's a simple, custom class I created to handle service-level errors. It looks like this:

public class ServiceException extends RuntimeException {

	private int statusCode;
	
	public ServiceException (String message, int statusCode) {
		super(message);
		this.statusCode = statusCode;
	}

	public int getStatusCode() {
		return statusCode;
	}
}

As you can see, ServiceException includes both a message and a status code. 

That's the exception that gets included in the Mono publisher. It's also the exception that gets thrown when the WebClient encounters a 405 HTTP status code.

Does This Handle Anything?

If you don't add the onStatus() method, you'll still get an exception. And it won't look much different than the exception you'll see if you run the code above.

So what's the advantage of onStatus()?

First of all, it's a great idea to throw back custom exceptions from the service layer rather than relying on framework exceptions. You can handle that with onStatus().

Then, your controller can use ServiceException to determine what kind of response to give back to the calling client. It will likely send back a ResponseEntity with a message derived from the ServiceException message. That ResponseEntity should also set the response status code from the status code in ServiceException.

Secondly, there might be some additional processing you need to take care of in response to a specific status code. 

So yeah, onStatus() can prove to be a quite useful animal in some situations.

Anyhoo, if you run the code above, you should get the following exception:

com.careydevelopment.crm.service.ServiceException: Method not allowed. Please check the URL.

And that means you got exactly what you were looking for.

Wrapping It Up

Now you know how to use onStatus(). Think about how it can be of use to you as you implement service-to-service level calls with WebClient.

Also, make a list of the kinds of response codes you want to handle and how you want to handle them.

But above all, have fun!

Photo by Andrea Piacquadio from Pexels