From cc641425bb1d10a47f345c69705dfc333d51e700 Mon Sep 17 00:00:00 2001 From: Brutus5000 Date: Sat, 27 Jun 2026 19:16:58 +0200 Subject: [PATCH 1/3] Publish RabbitMQ messages as JSON instead of Java serialization The avatar update event (and any other RabbitTemplate publish) was serialized with the default SimpleMessageConverter, producing application/x-java-serialized-object instead of JSON. The JacksonJsonMessageConverter was only wired into the listener container factory (consuming), not the auto-configured RabbitTemplate (publishing). Expose a single JacksonJsonMessageConverter bean so Spring Boot applies it to the auto-configured RabbitTemplate, and reuse it in the listener factory. Co-Authored-By: Claude Opus 4.8 --- .../api/config/RabbitConfiguration.java | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/faforever/api/config/RabbitConfiguration.java b/src/main/java/com/faforever/api/config/RabbitConfiguration.java index 13e5282f1..c2aed1524 100644 --- a/src/main/java/com/faforever/api/config/RabbitConfiguration.java +++ b/src/main/java/com/faforever/api/config/RabbitConfiguration.java @@ -55,16 +55,29 @@ public StatelessRetryOperationsInterceptor retryInterceptor() { * Reconfigure default Rabbit container, so it doesn't infinitely requeue. * Instead, we use a retry that does a limited requeueing. */ + /** + * JSON message converter used for both publishing (applied to the auto-configured + * {@link org.springframework.amqp.rabbit.core.RabbitTemplate} by Spring Boot when a single + * {@link org.springframework.amqp.support.converter.MessageConverter} bean is present) and consuming. + * Without this, the default {@code SimpleMessageConverter} would Java-serialize outgoing payloads + * ({@code application/x-java-serialized-object}) instead of producing JSON. + */ + @Bean + public JacksonJsonMessageConverter jacksonJsonMessageConverter() { + return new JacksonJsonMessageConverter(); + } + @Bean public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory( ConnectionFactory connectionFactory, - StatelessRetryOperationsInterceptor retryInterceptor + StatelessRetryOperationsInterceptor retryInterceptor, + JacksonJsonMessageConverter jacksonJsonMessageConverter ) { var factory = new SimpleRabbitListenerContainerFactory(); factory.setDefaultRequeueRejected(false); factory.setConnectionFactory(connectionFactory); factory.setAdviceChain(retryInterceptor); - factory.setMessageConverter(new JacksonJsonMessageConverter()); + factory.setMessageConverter(jacksonJsonMessageConverter); return factory; } From 80e07c6507e0240b97a09af588f104cb06b5e49f Mon Sep 17 00:00:00 2001 From: Brutus5000 Date: Sat, 27 Jun 2026 19:54:44 +0200 Subject: [PATCH 2/3] Add integration test verifying RabbitMQ payloads are JSON-encoded Publishes a player_avatar event through the real auto-configured RabbitTemplate (via PlayerAvatarUpdateHook) against a RabbitMQ Testcontainer and reads the raw message back, asserting the content type is application/json (not application/x-java-serialized-object) and the body is the expected JSON. Covers both an assigned avatar and the cleared (null) case. Co-Authored-By: Claude Opus 4.8 --- .../api/config/RabbitMessageEncodingTest.java | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 src/inttest/java/com/faforever/api/config/RabbitMessageEncodingTest.java diff --git a/src/inttest/java/com/faforever/api/config/RabbitMessageEncodingTest.java b/src/inttest/java/com/faforever/api/config/RabbitMessageEncodingTest.java new file mode 100644 index 000000000..3183334cf --- /dev/null +++ b/src/inttest/java/com/faforever/api/config/RabbitMessageEncodingTest.java @@ -0,0 +1,99 @@ +package com.faforever.api.config; + +import com.faforever.api.AbstractIntegrationTest; +import com.faforever.api.data.domain.Avatar; +import com.faforever.api.data.domain.Player; +import com.faforever.api.data.hook.PlayerAvatarUpdateHook; +import com.yahoo.elide.annotation.LifeCycleHookBinding.Operation; +import com.yahoo.elide.annotation.LifeCycleHookBinding.TransactionPhase; +import org.json.JSONException; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; +import org.springframework.amqp.core.AmqpAdmin; +import org.springframework.amqp.core.BindingBuilder; +import org.springframework.amqp.core.Message; +import org.springframework.amqp.core.MessageProperties; +import org.springframework.amqp.core.Queue; +import org.springframework.amqp.core.TopicExchange; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.beans.factory.annotation.Autowired; + +import java.nio.charset.StandardCharsets; +import java.util.Optional; + +import static com.faforever.api.config.RabbitConfiguration.EXCHANGE_FAF_LOBBY; +import static com.faforever.api.data.hook.PlayerAvatarUpdateHook.ROUTING_KEY_PLAYER_AVATAR_UPDATE; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Verifies that messages published through the auto-configured {@link RabbitTemplate} are serialized as JSON + * (content type {@code application/json}) rather than the default {@code application/x-java-serialized-object}, + * i.e. that the application-wide {@link RabbitConfiguration#jacksonJsonMessageConverter()} bean is actually + * applied to the publishing side. + */ +public class RabbitMessageEncodingTest extends AbstractIntegrationTest { + + private static final String TEST_QUEUE = "inttest.player_avatar.update"; + + @Autowired + private RabbitTemplate rabbitTemplate; + @Autowired + private AmqpAdmin amqpAdmin; + @Autowired + private PlayerAvatarUpdateHook playerAvatarUpdateHook; + + private void bindTestQueue() { + Queue queue = new Queue(TEST_QUEUE, false, false, true); + amqpAdmin.declareQueue(queue); + amqpAdmin.declareBinding( + BindingBuilder.bind(queue).to(new TopicExchange(EXCHANGE_FAF_LOBBY)).with(ROUTING_KEY_PLAYER_AVATAR_UPDATE)); + } + + @AfterEach + public void deleteTestQueue() { + amqpAdmin.deleteQueue(TEST_QUEUE); + } + + @Test + public void avatarUpdateIsPublishedAsJson() throws JSONException { + bindTestQueue(); + + Player player = new Player(); + player.setId(42); + Avatar avatar = new Avatar(); + avatar.setId(5); + player.setCurrentAvatar(avatar); + + playerAvatarUpdateHook.execute(Operation.UPDATE, TransactionPhase.POSTCOMMIT, player, null, Optional.empty()); + + Message message = rabbitTemplate.receive(TEST_QUEUE, 5000); + + assertThat(message).isNotNull(); + assertThat(message.getMessageProperties().getContentType()).isEqualTo(MessageProperties.CONTENT_TYPE_JSON); + JSONAssert.assertEquals( + "{\"player_id\":42,\"avatar_id\":5}", + new String(message.getBody(), StandardCharsets.UTF_8), + true); + } + + @Test + public void clearedAvatarIsPublishedAsJsonWithNull() throws JSONException { + bindTestQueue(); + + Player player = new Player(); + player.setId(42); + player.setCurrentAvatar(null); + + playerAvatarUpdateHook.execute(Operation.UPDATE, TransactionPhase.POSTCOMMIT, player, null, Optional.empty()); + + Message message = rabbitTemplate.receive(TEST_QUEUE, 5000); + + assertThat(message).isNotNull(); + assertThat(message.getMessageProperties().getContentType()).isEqualTo(MessageProperties.CONTENT_TYPE_JSON); + JSONAssert.assertEquals( + "{\"player_id\":42,\"avatar_id\":null}", + new String(message.getBody(), StandardCharsets.UTF_8), + true); + } +} From 9bc852f190f9ab347987a6a4ebae0cbb0525951d Mon Sep 17 00:00:00 2001 From: Brutus5000 Date: Sat, 27 Jun 2026 19:58:49 +0200 Subject: [PATCH 3/3] Use text blocks for expected JSON in encoding test Replaces escaped JSON string literals with multi-line text blocks for readability. Co-Authored-By: Claude Opus 4.8 --- .../api/config/RabbitMessageEncodingTest.java | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/inttest/java/com/faforever/api/config/RabbitMessageEncodingTest.java b/src/inttest/java/com/faforever/api/config/RabbitMessageEncodingTest.java index 3183334cf..fbd1aa327 100644 --- a/src/inttest/java/com/faforever/api/config/RabbitMessageEncodingTest.java +++ b/src/inttest/java/com/faforever/api/config/RabbitMessageEncodingTest.java @@ -71,8 +71,11 @@ public void avatarUpdateIsPublishedAsJson() throws JSONException { assertThat(message).isNotNull(); assertThat(message.getMessageProperties().getContentType()).isEqualTo(MessageProperties.CONTENT_TYPE_JSON); - JSONAssert.assertEquals( - "{\"player_id\":42,\"avatar_id\":5}", + JSONAssert.assertEquals(""" + { + "player_id": 42, + "avatar_id": 5 + }""", new String(message.getBody(), StandardCharsets.UTF_8), true); } @@ -91,8 +94,11 @@ public void clearedAvatarIsPublishedAsJsonWithNull() throws JSONException { assertThat(message).isNotNull(); assertThat(message.getMessageProperties().getContentType()).isEqualTo(MessageProperties.CONTENT_TYPE_JSON); - JSONAssert.assertEquals( - "{\"player_id\":42,\"avatar_id\":null}", + JSONAssert.assertEquals(""" + { + "player_id": 42, + "avatar_id": null + }""", new String(message.getBody(), StandardCharsets.UTF_8), true); }