Pollito Blog
January 23, 2024

Desarrollo basado en contratos 8: Mejora de la deserialización y el manejo de errores

Posted on January 23, 2024  •  7 minutes  • 1456 words  • Other languages:  English

Mejoras.

Consulta el repositorio de github

Esta es una continuación de Desarrollo basado en contratos 7: Tenemos el clima! .

Todo lo que haremos aquí, lo puedes encontrar en el repositorio de github.

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

Reemplazo del serializador por una extensión

En el último blog dije:

OpenAPI tiene una sección de extensiones de proveedores compatibles . Podemos chequearlo en algún momento en el futuro.

Bueno, ese día es hoy.

1. Eliminar WeatherDeserializer

Con esta nueva característica, ya no será necesario.

2. Reemplazar el uso de WeatherDeserializer en WeatherResponseDecoder

Ahora que no hay deserializador, en el decodificador no tenemos nada que registrar. En su lugar, tenemos que establecer una política de nombres de campos en minúsculas con guiones bajos.

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

3. Agregar la extensión en el archivo yaml de la OEA

En el campo localtime, que fue el que nos dio problema la última vez porque es una palabra reservada, añadimos la extensión necesaria. Esto agregará la anotación en tiempo de compilación.

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\")"

Podemos comprobarlo en el archivo generado Location.java

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

y ahora al ejecutar la aplicación, ¡todo funciona! Nice.

Manejo de errores con Controller Advice

Ahora mismo, cuando ocurre un error, dejamos que Java decida qué devolver. Creo que no es una buena práctica y, en su lugar, deberíamos optar por un Controller Advice.

Si no conoce esta implementación del paradigma AOP , te sugiero leer el blog ejemplo de @RestControllerAdvice en Spring Boot de bezkoder.

Estas son algunas de las razones a favor del uso de Controller Advice:

Respuestas de error amigables

En una API bien diseñada, es fundamental considerar la experiencia del consumidor del endpoint. Cuando se produce un error, devolver todo el stack de error no sólo es abrumador sino que tampoco ayuda al consumidor que intenta comprender qué salió mal.

Controller Advice en Spring nos permite elaborar mensajes de error claros, concisos y fáciles de usar. Este enfoque respeta el principio de fallar rápido y claramente, guiando al consumidor hacia una posible rectificación del problema sin exponerlo a la complejidad innecesaria de un stack trace.

Preocupaciones de seguridad al exponer el stack trace

El stack trace pueden revelar el funcionamiento interno de la aplicación, incluidas las estructuras de los paquetes, los nombres de las clases y, a veces, incluso las rutas de los archivos y los detalles de configuración.

Esta información puede ser una mina de oro para los usuarios malintencionados que buscan explotar vulnerabilidades. Con el Controller Advice, podemos controlar la salida, garantizando que la información confidencial permanezca dentro de los límites del servidor, adhiriéndose así a las mejores prácticas de seguridad.

Clasificación y manejo precisos de errores

El manejo de errores predeterminado de Spring a veces puede clasificar erróneamente los errores del usuario (400 bad request) como errores del servidor (500 internal server error).

Esto no sólo es engañoso sino que también puede desencadenar procedimientos de diagnóstico incorrectos. Al implementar los consejos del controlador de errores, obtenemos un control más preciso sobre la clasificación de errores.

Esta categorización precisa de errores no solo es útil para los consumidores de API, sino también para mantener y monitorear el estado de la aplicación.

Optimización en el manejo de errores con excepciones:

En las arquitecturas en capas tradicionales, la gestión de flujos de errores puede resultar engorrosa si pasa información de errores a través de varias capas (como del servicio al controlador) utilizando objetos personalizados o DTO.

Este enfoque a menudo conduce a un código inflado y complica la lógica, ya que cada capa necesita manejar y posiblemente transformar o aumentar la información de error.

Ahora, compare esto con la elegancia de usar excepciones combinadas con Controller Advice:

Arquitectura de Rest Controller Advice

La siguiente arquitectura es fuertemente opninionada por mi parte. Consta de dos partes:

Manejo de excepciones extraño cuando se trata de la API Weatherstack

Me gusta que la API Weatherstack se haya convertido en un ejemplo de todo lo que puede salirse de lo común a la hora de desarrollar una solución.

¿Qué pasa ahora con el servicio Weatherstack? Bueno, mira esta solicitud/respuesta: al consultar por una ciudad que no existe uno esperaría un 400 o 404, pero mira esto: weatherstack wrong response

200… eso está feo.

En sus docs mencionan códigos de error de API, pero nunca dicen que el estado de la respuesta será 200.

No podemos usar un decodificador de errores en WeatherApiConfig, porque Feign ve 200 y piensa que todo está bien. En lugar de eso, tenemos que lanzar una excepción personalizada en WeatherResponseDecoder.

Pero arrojar exceptiones aquí plantea otro problema: la excepción personalizada arrojada está encapsulada dentro de DecodeException, y nuestro WeatherControllerAdvice dice “oh, no conozco a este tipo, solo conozco WeatherException”.

¿Cómo resolverlo? Agreguando en la funcion que maneja Exception.class una condición que verifique si tal vez la excepción sea una WeatherException envuelta en una DecodeException. Si se cumple esa condición, delegar el manejo del error al Rest Controller Advice correspondiente.

En palabras suena demasiado complicado, pero en código se ve así:

@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;
}

No olvidar que no todas las WeatherException son solicitudes incorrectas porque no existe una ciudad. Esas son solo aquellas con errores marcados con estado 615 (sí, muy específico de nuestro proveedor de API).

Finalmente, agregar una verificación para este código. Si no, devolver un error genérico.

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;
}

Próximos pasos

Hey, check me out!

You can find me here