fix(notifications): defensive /me/notifications — wrap DB ops in try/catch

This commit is contained in:
Александр Зимин
2026-05-14 21:00:42 +00:00
parent 373215971d
commit 0cf76d23c1
@@ -72,21 +72,48 @@ public class MeNotificationsController {
String emailKey = currentUserEmail();
int safeLimit = Math.min(limit, MAX_LIMIT);
Page<NotificationLog> page;
if (emailKey != null && !emailKey.isBlank()) {
page = notificationLogRepository.findByRecipientOrderBySentAtDesc(
emailKey, PageRequest.of(0, safeLimit));
} else {
// No email in JWT — return empty, not an error
// Hardened against runtime exceptions — endpoint hit от bell on каждой
// странице; 500 здесь сломает global UX. Каждая под-операция wraped
// в try/catch с structured log + sensible fallback (empty list / 0 count).
// Backend can still see issues via logs; user sees empty drawer instead
// of "Внутренняя ошибка сервера".
if (emailKey == null || emailKey.isBlank()) {
// No email claim in JWT — can't look up notifications. Return empty.
log.debug("No email claim in JWT for user sub={}, returning empty notifications", currentUserSub);
return Map.of("items", List.of(), "totalUnread", 0);
}
List<NotificationItemDto> items = page.getContent().stream()
List<NotificationItemDto> items;
try {
Page<NotificationLog> page = notificationLogRepository.findByRecipientOrderBySentAtDesc(
emailKey, PageRequest.of(0, safeLimit));
items = page.getContent().stream()
.filter(n -> status == null || status.equalsIgnoreCase(n.getStatus().name()))
.map(n -> toDto(n, currentUserSub))
.map(n -> {
try {
return toDto(n, currentUserSub);
} catch (RuntimeException e) {
log.warn("Failed to map NotificationLog id={} to DTO: {}", n.getId(), e.getMessage());
return null;
}
})
.filter(java.util.Objects::nonNull)
.toList();
} catch (RuntimeException e) {
log.error("Failed to load notifications for user sub={}, email={}: {}",
currentUserSub, emailKey, e.getMessage(), e);
items = List.of();
}
long totalUnread = countUnread(currentUserSub, emailKey);
long totalUnread = 0L;
try {
totalUnread = countUnread(currentUserSub, emailKey);
} catch (RuntimeException e) {
log.error("Failed to count unread notifications for user sub={}: {}",
currentUserSub, e.getMessage(), e);
// Fallback to 0 — better to under-count than show user a 500.
}
return Map.of(
"items", items,
@@ -118,7 +145,6 @@ public class MeNotificationsController {
Page<NotificationLog> page = notificationLogRepository.findByRecipientOrderBySentAtDesc(
emailKey, PageRequest.of(0, MAX_LIMIT));
OffsetDateTime now = OffsetDateTime.now();
page.getContent().forEach(n -> markReadState(sub, n.getId()));
log.debug("Marked {} notifications read for user {}", page.getNumberOfElements(), sub);
}
@@ -160,10 +186,13 @@ public class MeNotificationsController {
}
private boolean isRead(String sub, Long notificationId) {
Integer count = jdbcTemplate.queryForObject(
// PostgreSQL COUNT(*) returns BIGINT — request Long.class to avoid
// narrowing-conversion edge cases в Spring's RowMapper. Was Integer.class
// which Spring auto-converts via rs.getInt() (works but type-imprecise).
Long count = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM notification_read_state WHERE user_id = ? AND notification_id = ?",
Integer.class, sub, notificationId);
return count != null && count > 0;
Long.class, sub, notificationId);
return count != null && count > 0L;
}
private static String buildPreview(NotificationLog n) {