From f436d1caee7dd1fc953a3725c924c24cd9a9a391 Mon Sep 17 00:00:00 2001 From: Brutus5000 Date: Sun, 21 Jun 2026 22:26:45 +0200 Subject: [PATCH 1/2] Allow null steps in AchievementUpdateRequest Jackson 3 (brought in by Spring Boot 4) defaults FAIL_ON_NULL_FOR_PRIMITIVES to true and treats absent JSON properties as null for primitive record components. The producer omits steps for REVEAL/UNLOCK operations, which Jackson 2 silently coerced to 0 but Jackson 3 now rejects, turning every such message into a poison pill that blocks the achievement queue. Switch steps to Integer and document via @Schema that it is only required for INCREMENT and SET_STEPS_AT_LEAST. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../faforever/api/achievements/AchievementUpdateRequest.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/faforever/api/achievements/AchievementUpdateRequest.java b/src/main/java/com/faforever/api/achievements/AchievementUpdateRequest.java index 649e52458..dc84752d1 100644 --- a/src/main/java/com/faforever/api/achievements/AchievementUpdateRequest.java +++ b/src/main/java/com/faforever/api/achievements/AchievementUpdateRequest.java @@ -1,10 +1,13 @@ package com.faforever.api.achievements; +import io.swagger.v3.oas.annotations.media.Schema; + record AchievementUpdateRequest( int playerId, String achievementId, Operation operation, - int steps + @Schema(description = "Required for INCREMENT and SET_STEPS_AT_LEAST; ignored (and may be null/omitted) for REVEAL and UNLOCK.", nullable = true) + Integer steps ) { public enum Operation { REVEAL, UNLOCK, INCREMENT, SET_STEPS_AT_LEAST From d98139d67d07f007f64313df0eee3172b44f33e7 Mon Sep 17 00:00:00 2001 From: Brutus5000 Date: Sun, 21 Jun 2026 22:29:02 +0200 Subject: [PATCH 2/2] Reject INCREMENT/SET_STEPS_AT_LEAST without steps Without this, a malformed payload would NPE on auto-unboxing inside the switch and produce a confusing stack trace. Throwing explicitly points straight at the contract violation. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../api/achievements/AchievementsController.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/faforever/api/achievements/AchievementsController.java b/src/main/java/com/faforever/api/achievements/AchievementsController.java index abf753428..f324edc82 100644 --- a/src/main/java/com/faforever/api/achievements/AchievementsController.java +++ b/src/main/java/com/faforever/api/achievements/AchievementsController.java @@ -21,9 +21,16 @@ public void update(AchievementUpdateRequest request) { switch (request.operation()) { case REVEAL -> throw new UnsupportedOperationException("REVEAL is not yet implemented"); case UNLOCK -> achievementService.unlock(request.playerId(), request.achievementId()); - case INCREMENT -> achievementService.increment(request.playerId(), request.achievementId(), request.steps()); + case INCREMENT -> achievementService.increment(request.playerId(), request.achievementId(), requireSteps(request)); case SET_STEPS_AT_LEAST -> - achievementService.setStepsAtLeast(request.playerId(), request.achievementId(), request.steps()); + achievementService.setStepsAtLeast(request.playerId(), request.achievementId(), requireSteps(request)); } } + + private static int requireSteps(AchievementUpdateRequest request) { + if (request.steps() == null) { + throw new IllegalArgumentException("steps is required for operation " + request.operation()); + } + return request.steps(); + } }