Let's face it: sometimes you don't always get the response that you're expecting. And when you're using the Spring WebFlux WebClient interface to make a request, you need to handle those error conditions.

Fortunately, you can do that fairly easily.

In this guide, I'll show you how I like to handle errors returned by WebClient requests. 

Of course, you're always free to just go straight to the code if that's how you roll.

Otherwise, read on.

[Please note: the code here has been refactored. Be sure to check out the article on handling empty responses with WebClient once you're done reading this guide.]

The Business Requirements

Your boss Smithers barges into your office, irate as usual.

"What's with all these exception stacktraces I'm seeing in the logs?!?" he asks. "I need you to handle these exception conditions more gracefully! We don't need mile-long stacktraces!"

He pauses for a few moments.

"That's all!" he says as he storms back out of your office.

You Have a Starting Point

In the previous guide, I showed you how to log the responses you get with WebClient requests. You're going to build on that here.

But first, it's time to create a new exception class.

Why? Because you want an exception class that includes both a message and a status code. 

You could use WebClientRequestException for that purpose, but that's married to the WebClient code. You need something more service-y. That way if you ever decide to use something other than WebClient to make these requests, you don't have to change the exception class that gets sent back to the caller.

Now with that in mind, create this exception class:

public class ServiceException extends RuntimeException {

	private static final long serialVersionUID = -7661881974219233311L;

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

	public int getStatusCode() {
		return statusCode;
	}
}

The new class extends RuntimeException because of the way that WebClient handles exceptions. Indeed, even WebClientResponseException is an unchecked exception. So just roll with it.

Beyond that, the exception stores the message just like every other exception. It also stores the response status code. That status code will be in the 400's or 500's because those are the error codes.

Fiddling With the Filter

Now update the filter class. Specifically change the method that handles logging errors as follows:

	private static Mono<ClientResponse> logBody(ClientResponse response) {
		if (response.statusCode() != null && (response.statusCode().is4xxClientError() || response.statusCode().is5xxServerError())) {
			return response.bodyToMono(String.class)
					.flatMap(body -> {
						LOG.debug("Body is {}", body);						
						return Mono.error(new ServiceException(body, response.rawStatusCode()));
					});
		} else {
			return Mono.just(response);
		}
	}

The big change there is the second return statement. It's now returning Mono.error() instead of Mono.just().

That's still a publisher (remember: Mono is a publisher) . Not only that, but it still publishes ClientResponse.

However, the difference is that this time the publisher will terminate with the given exception once it's got a subscriber.

That "given exception," by the way is the new ServiceException() you see inside Mono.error().

The ServiceException constructor accepts two parameters: the first parameter is the response body returned by the downstream service. The second parameter is the HTTP status code.

Seasoning the Service

Now you need to update UserService. Change the fetchUser() method:

    public SalesOwner fetchUser(String bearerToken) {
    	try {
	        SalesOwner salesOwner = userClient.get()
	                .uri("/user/me")
	                .header(HttpHeaders.AUTHORIZATION, bearerToken)
	                .retrieve()
	                .bodyToMono(SalesOwner.class)
	                .block();
	        
	
	        LOG.debug("User is " + salesOwner);
	        
	        return salesOwner;
    	} catch (WebClientResponseException we) {
    		throw new ServiceException (we.getMessage(), we.getRawStatusCode());
    	}
    }

The first thing to notice is that the whole method is now in a try/catch block. That's cool because you want to start handling these errors gracefully.

Note, however, that the method catches WebClientResponseException and not ServiceException. What gives?

The point of that catch block is to catch anything not caught by the filter. It's the last stand. It's the Alamo.

And when it does catch that exception, it creates a new ServiceException and throws it back.

That's it. Now you've got something that will handle your error situations.

So Come on, Man, Check This Out

Time to see if this works. Create some initialization code that looks like this:

@Component
public class ApplicationListenerInitialize implements ApplicationListener<ApplicationReadyEvent>  {
	
	@Autowired
	private UserService userService;
	
    public void onApplicationEvent(ApplicationReadyEvent event) {        	
    	
    	try {
	    	SalesOwner owner = userService.fetchUser("Bearer eyJhbGciOiJIUzU....");
	    	
	    	ObjectMapper objectMapper = new ObjectMapper();
	    	objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
	    	objectMapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY);
	    	System.err.println(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(owner));
    	} catch (ServiceException se) {
    		System.err.println("Error: " + se.getStatusCode() + " " + se.getMessage());
    	} catch (Exception e) {
    		e.printStackTrace();
    	}
    }
}

Pay attention to the first catch block. It catches ServiceException. That's where you can make some graceful moves with the error condition.

Here, it's just printing out the error and moving on.

Now to fully test this out I once again recoded the downstream service to intentionally throw a 400 (Bad Request) error with the message "You did something wrong."

And if you want to do exactly what I've done above, just grab yourself a bearer token by using Postman to login to the user service.

Now with that preamble out of the way. Start your Spring Boot application and wait for everything to load. Then pay attention to the red lettering that appears in your console log. It should look like this:

Error: 400 You did something wrong.

Bingo. That's exactly what you're looking for.

Wrapping It Up

Well that's one way to skin this cat. There are plenty of others.

Feel free to take the code you see above and modify it to suit your own business requirements. Also: include it in a controller class and return the appropriate status code back to the calling client.

As always, feel free to grab the code on GitHub.

Have fun!

Photo by alleksana from Pexels