Aceptado
Los microservicios de Nebula se comunican entre módulos mediante clientes @HttpExchange
bloqueantes (*BlockingClient), nunca por acceso directo a JPA/schema de otro módulo. Cuando
una llamada remota falla con un error de negocio, el servicio remoto responde con un body
serializado que representa un SimappeException (mensaje, lista de errores, lista de
soluciones y status HTTP), pero Spring solo entrega ese body como texto plano dentro de una
HttpClientErrorException.
Sin un mapeo centralizado, cada consumidor de un BlockingClient tendría que:
responseBodyAsString() de cada excepción capturada.Se requiere un mapeador reutilizable, ubicado en un paquete común (nebula-commons), que
reconstruya el SimappeException original a partir del body del error, con una estrategia de
fallback clara cuando el body no tiene el formato esperado (por ejemplo, errores 5xx sin JSON,
timeouts de gateway, o servicios que aún no implementan el contrato de error estándar).
Se implementa SimappeExceptionMapper (com.centrica.nebula.commons.mapper), un @Component
con un único método público mapToSimappeException(HttpClientErrorException e, String contexto):
Camino feliz: se deserializa responseBodyAsString() a SimappeErrorResponse
(com.centrica.nebula.models.common.record.resource) vía ObjectMapper. El campo status
del JSON (nombre de enum, ej. "NOT_FOUND") se convierte a HttpStatus con
HttpStatus.valueOf(String). Se reconstruye y retorna un SimappeException con el
message/errors/solutions originales del servicio remoto, preservando el status HTTP
real.
Camino de fallback: si el body no es parseable (JsonProcessingException), se registra
el error (log.error, incluyendo el body crudo y la excepción de parseo) y se lanza un
SimappeException genérico, construido con:
"Error al consultar " + contexto + " con código " + e.getStatusCode()errors/solutions vacíos (List.of())HttpStatus tomado directamente de e.getStatusCode() (no del body, que no se pudoEl parámetro contexto lo provee cada consumidor (ej. "consultar tercero",
"emitir consecutivo") para que el mensaje de fallback sea identificable sin depender del
body remoto.
El mapeador se inyecta (@RequiredArgsConstructor) en cada *BlockingClient o componente
que consuma servicios remotos, y se invoca dentro de su propio catch (HttpClientErrorException e).
Patrón de consumo estándar ya establecido en otros módulos (ej. Accounting), donde el método
que envuelve la llamada al client relanza el resultado del mapper directamente:
private TipoAsientoResponse obtenerTipoAsiento(String codigo, HttpServletRequest request) {
try {
return tipoAsientoClient.getByCode(authorization(request), codigo)
.getResponse()
.getContent();
} catch (HttpClientErrorException e) {
throw simappeExceptionMapper.mapToSimappeException(e, "Tipo Asiento");
}
}
Convenciones fijas de este patrón:
catch solo captura HttpClientErrorException (errores 4xx del cliente remoto);@HttpExchange/RestClient subyacente).throw simappeExceptionMapper.mapToSimappeException(...)contexto es una etiqueta corta y legible del recurso remoto consultado"Tipo Asiento", "Tercero", "Consecutivo"), usada tal cual en el mensaje de fallback —"consultar tipo de asiento por código", preferirSimappeErrorResponseEl contrato SimappeErrorResponse vive en nebula-models (no en commons), ya que representa
la forma del payload de error compartida por todos los módulos — no es una decisión de este ADR
per se, pero es un prerequisito: cualquier servicio remoto que no serialice sus errores en ese
formato exacto caerá siempre por el camino de fallback (comportamiento aceptado, no un bug).
Positivas
BlockingClientJsonProcessingException cruda hacia capasSimappeException manejable por el CustomResponseAdvisecontexto da trazabilidad mínima incluso cuando el body remoto es indescifrable.Negativas / riesgos aceptados
SimappeErrorResponse (JSON de otro contrato), Jackson puede lanzar JsonProcessingExceptionHttpStatus.valueOf(errorResponse.status()) asume que status es siempre un nombre de enumHttpStatus (ej. "NOT_FOUND"). Si el servicio remoto envía un código numérico"404") en vez del nombre, lanzará IllegalArgumentException, no capturada por elcatch (JsonProcessingException) actual — esta excepción se propagaría sin control. Es unException o se valida el formato