Pollito Dev
January 23, 2024

Contract-Driven Development 8: Improving deserialization and error handling

Posted on January 23, 2024  •  7 minutes  • 1329 words  • Other languages:  Español

Improvements time.

Check the github repo

This is a continuation of Contract-Driven Development 7: We get weather forecast! .

Everything we’ll do here, you can find in in the github repo.

Spring City Explorer - Backend: Branch feature/cdd-8

Replacing serializer in favor of an extension

In the last blog I said:

OpenAPI has a supported vendor extensions section . We can check that sometime in the future.

Well that day is today.

1. Delete WeatherDeserializer

With this new feature, is not longer gonna be needed.

2. Replace the use of WeatherDeserializer in WeatherResponseDecoder

Now that there’s no deserializer, in the decoder we don’t have anything to register. Instead, we have to set a field naming policy of lower case with underscores

return new GsonBuilder()
    .setFieldNamingPolicy(LOWER_CASE_WITH_UNDERSCORES)
    .create()
    .fromJson(responseBody, type);

3. Add the extension in the OAS yaml file

In the field localtime, which was the one that gave us problem last time cause is a reserved word, we add the extension needed. This will add the annotation on compile time.

localtime:
    type: string
    description: Returns the local time of the location used for this request.
    example: 2019-09-07 08:14
    x-field-extra-annotation: "@com.google.gson.annotations.SerializedName(\"localtime\")"

We can check it in the generated file Location.java

public static final String JSON_PROPERTY_LOCALTIME = "localtime";
@com.google.gson.annotations.SerializedName("localtime")
private String _localtime;

and now when running the application, everything work! That’s nice.

Error handling with Controller Advice

Right now, when an error occurs, we let java decide what to return. I find that is not a good practice, and instead we should go for a Controller Advice.

If you don’t know this AOP paradigm implementation, I strongly suggest read the blog @RestControllerAdvice example in Spring Boot by bezkoder.

Here are some of the reasons in favor of usage of Controller Advice:

User friendly error responses

In a well-designed API, it’s crucial to consider the experience of the endpoint consumer. When an error occurs, sending back a complete stack trace is not just overwhelming but also unhelpful for the consumer trying to understand what went wrong.

An error controller advice in Spring allows us to craft clear, concise, and user-friendly error messages. This approach respects the principle of failing fast and failing clearly, guiding the consumer towards potentially rectifying the issue without exposing them to the unnecessary complexity of a full stack trace.

Security concerns with exposing stack traces

Stack traces can reveal internal workings of the application, including package structures, class names, and sometimes even file paths and configuration details.

This information can be a gold mine for malicious users looking to exploit vulnerabilities. With error controller advice, we can control the output, ensuring that sensitive information stays within the confines of the server, thus adhering to best practices in security.

Accurate error classification and handling

Spring’s default error handling might sometimes misclassify user errors (like 400 Bad Requests) as server errors (500 Internal Server Error).

This is not just misleading but can also trigger incorrect diagnostic procedures. By implementing error controller advice, we gain finer control over error classification.

This accurate error categorization is not only helpful for API consumers but also for maintaining and monitoring the health of the application.

Streamline error handling with Exceptions:

In traditional layered architectures, anaging error flows can become cumbersome if you’re passing error information through various layers (like from service to controller) using custom objects or DTOs.

This approach often leads to bloated code and complicates the logic, as each layer needs to handle and possibly transform or augment the error information.

Now, contrast this with the elegance of using exceptions combined with controller advice:

Architecting Rest Controller Advice

The following architecture is very opinionated by me. It consists in two parts:

Weird exception handling when it comes to weatherstack API

I like that weatherstack API has became an example of everything that can be out of the common when developing a solution.

What’s wrong now with weatherstack? Well look at this request/response: I query for a non existing city. You would expect a 400 or 404, but look at this: weatherstack wrong response

200… oh that’s bad.

In their docs they mention API Error Codes, but never a thing that the status of the response will be 200.

We are not able to use an error decoder in WeatherApiConfig, cause Feign see 200 and thinks everything is OK. Instead we have to throw a custom exception in WeatherResponseDecoder.

But throwing expection here raises another problem: the custom exception thrown is encapsuled inside of DecodeException, and our Weather Rest Controller Advice says “oh, I don’t know this guy, I only know WeatherException.”

How to solve it? Add in the handler of Exception.class a condition checking if maybe the exception is a WeatherException wrapped in a DecodeException. If that condition is met, then delegate handling of the error to the correspondant Rest Controller Advice.

In words sounds too complicated, but in code looks something like this:

@ExceptionHandler(Exception.class)
public ResponseEntity<Error> handle(Exception e) {
if (isWeatherException(e)) {
    return weatherControllerAdvice.handle((WeatherException) e.getCause());
}
return getGenericError(e);
}

private boolean isWeatherException(Exception e) {
return e instanceof DecodeException && e.getCause() instanceof WeatherException;
}

Don’t forget that not every WeatherException is a Bad Request cause a city doesn’t exists. That is only those error marked with status 615 (yes, very specific of our API provider).

So finally, add a verification for this code. If not, we return a generic error.

public static final int BAD_REQUEST_ERROR_CODE = 615;

@ExceptionHandler(WeatherException.class)
public ResponseEntity<Error> handle(WeatherException e) {
if (isBadRequest(e)) {
    return getWeatherBadRequestError(e);
}
return getGenericError(e);
}

private boolean isBadRequest(WeatherException e) {
return Objects.nonNull(e.getWeatherStackError().getError())
    && Objects.nonNull(e.getWeatherStackError().getError().getCode())
    && e.getWeatherStackError().getError().getCode() == BAD_REQUEST_ERROR_CODE;
}

Next steps

Hey, check me out!

You can find me here