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(); String emailKey = currentUserEmail();
int safeLimit = Math.min(limit, MAX_LIMIT); int safeLimit = Math.min(limit, MAX_LIMIT);
Page<NotificationLog> page; // Hardened against runtime exceptions — endpoint hit от bell on каждой
if (emailKey != null && !emailKey.isBlank()) { // странице; 500 здесь сломает global UX. Каждая под-операция wraped
page = notificationLogRepository.findByRecipientOrderBySentAtDesc( // в try/catch с structured log + sensible fallback (empty list / 0 count).
emailKey, PageRequest.of(0, safeLimit)); // Backend can still see issues via logs; user sees empty drawer instead
} else { // of "Внутренняя ошибка сервера".
// No email in JWT — return empty, not an error
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); 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())) .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(); .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( return Map.of(
"items", items, "items", items,
@@ -118,7 +145,6 @@ public class MeNotificationsController {
Page<NotificationLog> page = notificationLogRepository.findByRecipientOrderBySentAtDesc( Page<NotificationLog> page = notificationLogRepository.findByRecipientOrderBySentAtDesc(
emailKey, PageRequest.of(0, MAX_LIMIT)); emailKey, PageRequest.of(0, MAX_LIMIT));
OffsetDateTime now = OffsetDateTime.now();
page.getContent().forEach(n -> markReadState(sub, n.getId())); page.getContent().forEach(n -> markReadState(sub, n.getId()));
log.debug("Marked {} notifications read for user {}", page.getNumberOfElements(), sub); log.debug("Marked {} notifications read for user {}", page.getNumberOfElements(), sub);
} }
@@ -160,10 +186,13 @@ public class MeNotificationsController {
} }
private boolean isRead(String sub, Long notificationId) { 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 = ?", "SELECT COUNT(*) FROM notification_read_state WHERE user_id = ? AND notification_id = ?",
Integer.class, sub, notificationId); Long.class, sub, notificationId);
return count != null && count > 0; return count != null && count > 0L;
} }
private static String buildPreview(NotificationLog n) { private static String buildPreview(NotificationLog n) {