From 663bb32b2c528c7a5a4bd3eec20e5bd5c927ac05 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Wed, 15 Jul 2026 13:19:06 +0100 Subject: [PATCH] Skip unreadable policy source/policy rows instead of crashing :) (#7034) # Description of Changes Thanks james for the prod issue :) --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have run `task check` to verify linters, typechecks, and tests pass - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#7-testing) for more details. --- .../policy/source/JpaSourceStore.java | 28 ++++++++++++++--- .../policy/store/JpaPolicyStore.java | 31 ++++++++++++++++--- 2 files changed, 49 insertions(+), 10 deletions(-) diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/source/JpaSourceStore.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/source/JpaSourceStore.java index 18bbf5cbdc..69249fb700 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/policy/source/JpaSourceStore.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/source/JpaSourceStore.java @@ -8,6 +8,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProp import org.springframework.stereotype.Service; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import tools.jackson.databind.ObjectMapper; @@ -15,6 +16,7 @@ import tools.jackson.databind.ObjectMapper; * Durable {@link SourceStore} backed by JPA; the runtime store. Sources are persisted as JSON via * {@link SourceEntity}, with scalar columns kept in sync for querying. */ +@Slf4j @Service @RequiredArgsConstructor @ConditionalOnBooleanProperty(name = "policies.enabled") @@ -53,17 +55,20 @@ public class JpaSourceStore implements SourceStore { @Override public Optional get(String id) { - return repository.findById(id).map(this::toSource); + return repository.findById(id).flatMap(this::toSource); } @Override public List all() { - return repository.findAll().stream().map(this::toSource).toList(); + return repository.findAll().stream().map(this::toSource).flatMap(Optional::stream).toList(); } @Override public List findByTeam(Long teamId) { - return repository.findByTeam(teamId).stream().map(this::toSource).toList(); + return repository.findByTeam(teamId).stream() + .map(this::toSource) + .flatMap(Optional::stream) + .toList(); } @Override @@ -75,7 +80,20 @@ public class JpaSourceStore implements SourceStore { return true; } - private Source toSource(SourceEntity entity) { - return objectMapper.readValue(entity.getSourceJson(), Source.class); + // Skip (don't fail) rows whose JSON can't be read - e.g. written by another app version/key. + // One unreadable row must never abort a bulk read or crash startup. + private Optional toSource(SourceEntity entity) { + try { + return Optional.of(objectMapper.readValue(entity.getSourceJson(), Source.class)); + } catch (Exception e) { + log.error( + "Skipping unreadable policy source id={} name={}: stored JSON could not be" + + " parsed ({}). Likely written by a different app version or" + + " encryption key.", + entity.getId(), + entity.getName(), + e.getMessage()); + return Optional.empty(); + } } } diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/store/JpaPolicyStore.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/store/JpaPolicyStore.java index 5089b8d419..08b2942b14 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/policy/store/JpaPolicyStore.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/store/JpaPolicyStore.java @@ -10,6 +10,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import stirling.software.proprietary.policy.model.Policy; @@ -19,6 +20,7 @@ import tools.jackson.databind.ObjectMapper; * Durable {@link PolicyStore} backed by JPA; the runtime store. Policies are persisted as JSON via * {@link PolicyEntity}, with scalar columns kept in sync for querying. */ +@Slf4j @Service @RequiredArgsConstructor @ConditionalOnBooleanProperty(name = "policies.enabled") @@ -98,23 +100,30 @@ public class JpaPolicyStore implements PolicyStore { @Override public Optional get(String id) { - return repository.findById(id).map(this::toPolicy); + return repository.findById(id).flatMap(this::toPolicy); } @Override public List all() { - return repository.findAllOrdered().stream().map(this::toPolicy).toList(); + return repository.findAllOrdered().stream() + .map(this::toPolicy) + .flatMap(Optional::stream) + .toList(); } @Override public List findByTeam(Long teamId) { - return repository.findByTeam(teamId).stream().map(this::toPolicy).toList(); + return repository.findByTeam(teamId).stream() + .map(this::toPolicy) + .flatMap(Optional::stream) + .toList(); } @Override public List findByTriggerType(String triggerType) { return repository.findByTriggerTypeAndEnabledTrue(triggerType).stream() .map(this::toPolicy) + .flatMap(Optional::stream) .toList(); } @@ -127,7 +136,19 @@ public class JpaPolicyStore implements PolicyStore { return true; } - private Policy toPolicy(PolicyEntity entity) { - return objectMapper.readValue(entity.getPolicyJson(), Policy.class); + // Skip (don't fail) rows whose JSON can't be read - e.g. written by another app version/key. + // One unreadable row must never abort a bulk read or crash startup. + private Optional toPolicy(PolicyEntity entity) { + try { + return Optional.of(objectMapper.readValue(entity.getPolicyJson(), Policy.class)); + } catch (Exception e) { + log.error( + "Skipping unreadable policy id={} name={}: stored JSON could not be parsed" + + " ({}). Likely written by a different app version or encryption key.", + entity.getId(), + entity.getName(), + e.getMessage()); + return Optional.empty(); + } } }