diff --git a/src/main/java/com/eactive/apim/portal/apps/ReadinessController.java b/src/main/java/com/eactive/apim/portal/apps/ReadinessController.java index 125bb9f..230d425 100644 --- a/src/main/java/com/eactive/apim/portal/apps/ReadinessController.java +++ b/src/main/java/com/eactive/apim/portal/apps/ReadinessController.java @@ -10,14 +10,21 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; +import org.thymeleaf.spring5.SpringTemplateEngine; /** * Readiness probe. * *

Distinct from {@link HealthCheckController} (liveness — servlet alive?). * This endpoint validates both EMS and Gateway datasources via JDBC - * {@code Connection.isValid(timeout)} to confirm the app is ready to serve - * requests that depend on the database. + * {@code Connection.isValid(timeout)}, and the Thymeleaf {@link SpringTemplateEngine} + * configuration, to confirm the app is ready to serve real page requests. + * + *

The template-engine check exists because this controller bypasses view + * resolution entirely (plain {@code @RestController} JSON) — a broken + * {@code TemplateEngine.getConfiguration()} (e.g. classpath split causing + * {@link java.util.ServiceConfigurationError} during dialect/module discovery) + * previously left every real page returning 500 while this probe still reported 200. * *

HTTP 200 + JSON when all checks pass. *

HTTP 503 + JSON when any check fails — body still includes the per-component @@ -30,24 +37,29 @@ public class ReadinessController { private final DataSource portalDataSource; private final DataSource gatewayDataSource; + private final SpringTemplateEngine templateEngine; public ReadinessController( @Qualifier("portalDataSource") DataSource portalDataSource, - @Qualifier("gatewayDataSource") DataSource gatewayDataSource) { + @Qualifier("gatewayDataSource") DataSource gatewayDataSource, + SpringTemplateEngine templateEngine) { this.portalDataSource = portalDataSource; this.gatewayDataSource = gatewayDataSource; + this.templateEngine = templateEngine; } @GetMapping(value = "/health/ready", produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity ready() { - String ems = check(portalDataSource); - String gw = check(gatewayDataSource); - boolean ok = "UP".equals(ems) && "UP".equals(gw); + String ems = check(portalDataSource); + String gw = check(gatewayDataSource); + String view = checkTemplateEngine(); + boolean ok = "UP".equals(ems) && "UP".equals(gw) && "UP".equals(view); String body = "{" - + "\"status\":\"" + (ok ? "UP" : "DOWN") + "\"," - + "\"ems\":\"" + ems + "\"," - + "\"gateway\":\""+ gw + "\"" + + "\"status\":\"" + (ok ? "UP" : "DOWN") + "\"," + + "\"ems\":\"" + ems + "\"," + + "\"gateway\":\"" + gw + "\"," + + "\"view\":\"" + view + "\"" + "}"; return ResponseEntity.status(ok ? 200 : 503) @@ -55,6 +67,20 @@ public class ReadinessController { .body(body); } + /** {@code getConfiguration()} lazily runs Thymeleaf's dialect/module init on first call + * and caches it — so this is cheap once healthy, and reproduces the exact failure path + * a real page render would hit. Catches {@link Throwable}: init failures here have + * surfaced as {@link Error} (ServiceConfigurationError), not just Exception. */ + private String checkTemplateEngine() { + try { + templateEngine.getConfiguration(); + return "UP"; + } catch (Throwable t) { + String msg = t.getMessage() == null ? t.getClass().getSimpleName() : t.getMessage(); + return "DOWN:" + msg.replace('"', '\'').replace('\n', ' ').replace('\r', ' '); + } + } + private static String check(DataSource ds) { if (ds == null) return "DOWN:NO_DATASOURCE"; try (Connection c = ds.getConnection()) {