Merge branch 'fix/notifications-controller-panic-defense' into 'main'
fix(notifications): panic-level defensive GET /me/notifications See merge request 2-6/2-6-4/terravault/ordinis!196
This commit is contained in:
+88
-46
@@ -21,6 +21,7 @@ import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -67,58 +68,99 @@ public class MeNotificationsController {
|
||||
public Map<String, Object> list(
|
||||
@RequestParam(required = false) String status,
|
||||
@RequestParam(defaultValue = "20") int limit) {
|
||||
|
||||
String currentUserSub = requireCurrentUserSub();
|
||||
String emailKey = currentUserEmail();
|
||||
int safeLimit = Math.min(limit, MAX_LIMIT);
|
||||
|
||||
// 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;
|
||||
// Panic-level defensiveness: this endpoint is hit by the bell badge
|
||||
// poll on EVERY page (every 30s). A 500 here breaks global UX, even
|
||||
// for users who don't care about notifications.
|
||||
//
|
||||
// Strategy: wrap the entire handler in try/catch. Anonymous request
|
||||
// OR unauthenticated context returns empty cleanly (no 403 throw —
|
||||
// bell на logged-out landing not worth a guard violation). Any other
|
||||
// failure also returns empty + logs the cause for follow-up.
|
||||
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 -> {
|
||||
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();
|
||||
}
|
||||
String sub = currentUserSubSafe();
|
||||
if (sub == null) {
|
||||
// Anonymous / no JWT → no notifications possible. Don't throw 403,
|
||||
// just say "no notifications" — semantically true.
|
||||
return emptyResponse();
|
||||
}
|
||||
|
||||
long totalUnread = 0L;
|
||||
String emailKey = currentUserEmailSafe();
|
||||
if (emailKey == null || emailKey.isBlank()) {
|
||||
// Authenticated but no email claim. Recipient lookup uses email,
|
||||
// so without it we have nothing to fetch.
|
||||
log.debug("No email claim in JWT for user sub={}", sub);
|
||||
return emptyResponse();
|
||||
}
|
||||
|
||||
int safeLimit = Math.min(Math.max(limit, 1), MAX_LIMIT);
|
||||
|
||||
List<NotificationItemDto> items;
|
||||
try {
|
||||
Page<NotificationLog> page = notificationLogRepository.findByRecipientOrderBySentAtDesc(
|
||||
emailKey, PageRequest.of(0, safeLimit));
|
||||
items = page.getContent().stream()
|
||||
.filter(n -> status == null || (n.getStatus() != null && status.equalsIgnoreCase(n.getStatus().name())))
|
||||
.map(n -> {
|
||||
try {
|
||||
return toDto(n, sub);
|
||||
} 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={}: {}",
|
||||
sub, emailKey, e.getMessage(), e);
|
||||
items = List.of();
|
||||
}
|
||||
|
||||
long totalUnread = 0L;
|
||||
try {
|
||||
totalUnread = countUnread(sub, emailKey);
|
||||
} catch (RuntimeException e) {
|
||||
log.error("Failed to count unread notifications for user sub={}: {}",
|
||||
sub, e.getMessage(), e);
|
||||
}
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("items", items);
|
||||
response.put("totalUnread", totalUnread);
|
||||
return response;
|
||||
} catch (RuntimeException e) {
|
||||
// Last-resort catch: anything we missed. Don't propagate to the
|
||||
// global handler (which would 500). Log and return empty.
|
||||
log.error("Unexpected error in GET /me/notifications", e);
|
||||
return emptyResponse();
|
||||
}
|
||||
}
|
||||
|
||||
private static Map<String, Object> emptyResponse() {
|
||||
Map<String, Object> r = new HashMap<>();
|
||||
r.put("items", List.of());
|
||||
r.put("totalUnread", 0L);
|
||||
return r;
|
||||
}
|
||||
|
||||
/** Returns sub claim from JWT or null if missing/anonymous. Never throws. */
|
||||
private String currentUserSubSafe() {
|
||||
try {
|
||||
totalUnread = countUnread(currentUserSub, emailKey);
|
||||
return currentUserSub();
|
||||
} 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.
|
||||
log.warn("Failed to read JWT sub claim: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return Map.of(
|
||||
"items", items,
|
||||
"totalUnread", totalUnread
|
||||
);
|
||||
/** Returns email claim from JWT or null. Never throws. */
|
||||
private String currentUserEmailSafe() {
|
||||
try {
|
||||
return currentUserEmail();
|
||||
} catch (RuntimeException e) {
|
||||
log.warn("Failed to read JWT email claim: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user