You are viewing a plain text version of this content. The canonical link for it is here.
Posted to issues@camel.apache.org by "Claus Ibsen (Jira)" <ji...@apache.org> on 2025/04/20 08:58:00 UTC

[jira] [Resolved] (CAMEL-21912) camel-cxf - Caching of ToStringTypeConverter breaks CXF RS Rest service

     [ https://issues.apache.org/jira/browse/CAMEL-21912?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel ]

Claus Ibsen resolved CAMEL-21912.
---------------------------------
    Resolution: Fixed

> camel-cxf - Caching of ToStringTypeConverter breaks CXF RS Rest service
> -----------------------------------------------------------------------
>
>                 Key: CAMEL-21912
>                 URL: https://issues.apache.org/jira/browse/CAMEL-21912
>             Project: Camel
>          Issue Type: Bug
>          Components: camel-core, camel-cxf
>    Affects Versions: 4.2.0, 4.4.5, 4.8.5, 4.10.2
>         Environment: Camel 4.2.0 (or later) - tested with 4.10.2
> Java 21
> Spring Boot 3.4.4
> CXF 4.1.1
>            Reporter: Dave Riseley
>            Assignee: Claus Ibsen
>            Priority: Minor
>             Fix For: 4.8.7, 4.10.4, 4.12.0
>
>         Attachments: ArrayListConverterTests.java, pom.xml
>
>
> We have encountered an issue where logging an ArrayList<T> in one camel route can break a CXF JaxRS service in another route.
>  
> After some considerable debugging, it appears that the logging of the form:
> {{log(LoggingLevel.INFO, "Logging body: ${body}")}}
> causes a {{ToStringTypeConverter}} to be cached for the {{ArrayList}} to {{String type}} conversion.
> This in turn causes JaxRS Rest service to break as the cxf {{MessageContentsList}} to {{String}} conversion incorrectly uses the {{ToStringTypeConverter}} instead of the {{SpringTypeConverter}} converter
> A full test is shown below.  I will attach the test and the pom.xml required to build it.
> In the test below a simple CxfRS echo service is created, where the response is the value of the {{say}} query parameter.
> For a call to http://localhost:8001/echo?say=hello we would expect the response payload of {{"hello"}}.  When bug is triggered (by logging an arraylist in a separate route), the actual response payload returned is {{"[hello]"}} (notice the additional square brackets)
> In my opinion, logging shouldn't be able to cause the side effects seen here.  This behaviour appears to have been introduced in Camel 4.2.0 by either (or both) of CAMEL-20051 and CAMEL-19398, and is present in the latest version 4.10.2
> [~orpiske] FYI.
> {code:java}
> package com.example.arrayconverter;
> import static org.junit.jupiter.api.Assertions.*;
> import java.util.ArrayList;
> import java.util.List;
> import org.apache.camel.Exchange;
> import org.apache.camel.LoggingLevel;
> import org.apache.camel.ProducerTemplate;
> import org.apache.camel.RoutesBuilder;
> import org.apache.camel.TypeConverter;
> import org.apache.camel.builder.RouteBuilder;
> import org.apache.camel.component.cxf.spring.jaxrs.SpringJAXRSServerFactoryBean;
> import org.apache.camel.impl.converter.DefaultTypeConverter;
> import org.apache.camel.test.spring.junit5.CamelSpringBootTest;
> import org.apache.cxf.Bus;
> import org.apache.cxf.message.MessageContentsList;
> import org.junit.jupiter.api.Order;
> import org.junit.jupiter.api.Test;
> import org.springframework.beans.factory.annotation.Autowired;
> import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
> import org.springframework.boot.test.context.SpringBootTest;
> import org.springframework.context.annotation.Bean;
> import org.springframework.context.annotation.Configuration;
> import org.springframework.test.annotation.DirtiesContext;
> import org.springframework.test.annotation.DirtiesContext.ClassMode;
> import jakarta.ws.rs.*;
> import jakarta.ws.rs.core.*;
> @CamelSpringBootTest
> @EnableAutoConfiguration
> @SpringBootTest
> @DirtiesContext(classMode = ClassMode.AFTER_EACH_TEST_METHOD)
> class ArrayListConverterTests {
>     @Autowired
>     DefaultTypeConverter defaultTypeConverter;
>     @Autowired
>     ProducerTemplate producerTemplate;
>     @Configuration
>     static class TestConfig {
>         @Bean
>         SpringJAXRSServerFactoryBean restServer(Bus bus) {
>             SpringJAXRSServerFactoryBean restServer = new SpringJAXRSServerFactoryBean();
>             restServer.setBus(bus);
>             restServer.setAddress("http://localhost:8001/echo");
>             restServer.setServiceClass(EchoService.class);
>             return restServer;
>         }
>         @Bean
>         RoutesBuilder loggingRoute() {
>             return new RouteBuilder() {
>                 @Override
>                 public void configure() throws Exception {
>                     from("direct:logging").log(LoggingLevel.INFO, "Logging body: ${body}");
>                 }
>             };
>         }
>         @Bean
>         RoutesBuilder echoService() {
>             return new RouteBuilder() {
>                 @Override
>                 public void configure() throws Exception {
>                     from("cxfrs:bean:restServer").bean(new EchoServiceImpl());
>                 }
>             };
>         }
>     }
>     private record Fruit(String name) {
>     }
>     private interface EchoService {
>         @GET
>         @Path("/")
>         @Produces(MediaType.TEXT_PLAIN)
>         public Response echo(@QueryParam("say") String say);
>     }
>     private static class EchoServiceImpl implements EchoService {
>         @Override
>         public Response echo(String say) {
>             return Response.ok().entity(say).build();
>         }
>     }
>     @Test
>     @Order(1)
>     void testSuccessfulCxfRestCallWithoutCallingLoggingRoute() {
>         
>         // Call the echo CXF Rest service
>         Exchange responseExchange = producerTemplate.send("http://localhost:8001/echo?say=hello", e -> {});
>         assertNull(responseExchange.getException());
>         assertEquals(200, responseExchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE, Integer.class));
>         // This is the expected output - the value of the say query paramater
>         assertEquals("hello", responseExchange.getMessage().getBody(String.class));
>         // There is no List -> String type convertor
>         TypeConverter t = defaultTypeConverter.getTypeConverter(String.class, ArrayList.class);
>         assertNull(t);
>          // The MessageContentsList -> String type coverter is the expected SpringTypeConverter
>         t = defaultTypeConverter.getTypeConverter(String.class, MessageContentsList.class);
>         assertNotNull(t);
>         assertEquals(org.apache.camel.spring.boot.SpringTypeConverter.class, t.getClass());
>     }
>     
>     @Test
>     @Order(2)
>     void testFailedCxfRestCallWithCallingLoggingRoute() {
>         // There is no List -> String type convertor
>         TypeConverter t = defaultTypeConverter.getTypeConverter(String.class, ArrayList.class);
>         assertNull(t);
>         // Trigger logging of an array of records in one route
>         producerTemplate.sendBody("direct:logging", 
>             new ArrayList<Fruit>(
>                 List.of(
>                     new Fruit("apples"), 
>                     new Fruit("bananas"), 
>                     new Fruit("carrots")
>                 )));
>         // Notice a List -> String type converter has been registered
>         t = defaultTypeConverter.getTypeConverter(String.class, ArrayList.class);
>         assertNotNull(t);
>         assertEquals(org.apache.camel.impl.converter.ToStringTypeConverter.class, t.getClass());
>         /// Call the echo CXF Rest service in separate route
>         Exchange responseExchange = producerTemplate.send("http://localhost:8001/echo?say=hello", e -> {});
>         assertNull(responseExchange.getException());
>         assertEquals(200, responseExchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE, Integer.class));
>         
>         // THIS IS THE FAILURE - the logging in the first route has impacted the format of the payload
>         assertEquals("[hello]", responseExchange.getMessage().getBody(String.class));
>         // The type converter above overrides what should be the the SpringTypeConverter
>         t = defaultTypeConverter.getTypeConverter(String.class, MessageContentsList.class);
>         assertNotNull(t);
>         assertEquals(org.apache.camel.impl.converter.ToStringTypeConverter.class, t.getClass());
>     }
> }
> {code}



--
This message was sent by Atlassian Jira
(v8.20.10#820010)