Dave Riseley created CAMEL-21912:
------------------------------------
Summary: 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.10.2, 4.8.5, 4.4.5, 4.2.0
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
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)