fix(notifications): defensive /me/notifications — wrap DB ops in try/catch
This commit is contained in:
+44
-15
@@ -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;
|
||||||
.filter(n -> status == null || status.equalsIgnoreCase(n.getStatus().name()))
|
try {
|
||||||
.map(n -> toDto(n, currentUserSub))
|
Page<NotificationLog> page = notificationLogRepository.findByRecipientOrderBySentAtDesc(
|
||||||
.toList();
|
emailKey, PageRequest.of(0, safeLimit));
|
||||||
|
items = page.getContent().stream()
|
||||||
|
.filter(n -> status == null || status.equalsIgnoreCase(n.getStatus().name()))
|
||||||
|
.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(
|
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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user