I have a Spring Boot 1.3.6 application, built out of the box and using the embedded Tomcat server. The application has a single endpoint doing a very simple echo request.
Later I defined a corresponding client invoking that simple endpoint using AsyncRestTemplate
, however if my client uses the Netty4ClientHttpRequestFactory
the request fails, otherwise it succeeds.
My example below is in Kotlin, but it fails just the same in Java, so it does not have to do with the language I use to implement it.
@SpringBootApplication
open class EchoApplication {
companion object {
@JvmStatic fun main(args: Array<String>) {
SpringApplication.run(EchoApplication::class.java, *args)
}
}
@Bean
open fun objectMapper(): ObjectMapper {
return ObjectMapper().apply {
dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX")
registerModule(KotlinModule())
}
}
@Bean
open fun customConverters(): HttpMessageConverters {
return HttpMessageConverters(listOf(MappingJackson2HttpMessageConverter(objectMapper())))
}
}
My endpoint looks like this:
@RestController
class EchoController {
@RequestMapping(value="/foo", method = arrayOf(RequestMethod.PUT))
fun echo(@RequestBody order: Order): Order {
return order
}
}
And the order data class is
data class Order(val orderId: String)
Note: Since I use Kotlin, I also added the Kotlin Jackson Module to ensure proper constructor deserialization.
I then proceeded to create a client that invokes this endpoint.
If I do something like the following in my client, it works perfectly and I get a successful echo response.
val executor = TaskExecutorAdapter(Executors.newFixedThreadPool(2))
val restTemplate = AsyncRestTemplate(executor)
restTemplate.messageConverters = listOf(MappingJackson2HttpMessageConverter(mapper))
val promise = restTemplate.exchange(URI.create("http://localhost:8080/foo"), HttpMethod.PUT, HttpEntity(Order("b1254"), headers), Order::class.java)
promise.addCallback(object : ListenableFutureCallback<ResponseEntity<Order>> {
override fun onSuccess(result: ResponseEntity<Order>) {
println(result.body)
}
override fun onFailure(ex: Throwable) {
ex.printStackTrace()
if(ex is HttpStatusCodeException){
println(ex.responseBodyAsString)
}
}
})
As mentioned above, the code above runs perfectly and prints a successful echo response.
But if I decide to use the Netty client, then I get 400 Bad Request reporting I did not pass the body:
val nettyFactory = Netty4ClientHttpRequestFactory()
val restTemplate = AsyncRestTemplate(nettyFactory)
When I do this then I get a HttpMessageNotReadableException
with a message saying "Required request body is missing".
I debugged the Spring Boot code and I see that when the ServletInputStream
is read, it always return -1 as if it was empty.
In my gradle I added runtime('io.netty:netty-all:4.1.2.Final')
, so I am using what, as of today, is the latest version of Netty. This Netty version has worked just fine when interacting with endpoints in other projects that I have that use regular Spring (i.e. not Spring Boot).
How come the SimpleClientHttpRequestFactory
works fine, but the Netty4ClientHttpRequestFactory
fails?
I thought it might had to do with the embedded Tomcat server, however, if I package this application as war and deploy it in an existing Tomcat server (i.e. not using the embedded one), the problem persists. So, I'm guessing is something related to Spring/Spring Boot.
Am I missing any configuration in my Spring Boot app? Any suggestions on how to make the Netty client work with Spring Boot?
Looks like there are problem in serialization on Client's side. Because this code works perfect:
restTemplate.exchange(
URI.create("http://localhost:8080/foo"),
HttpMethod.PUT,
HttpEntity("""{"orderId":"1234"}""", HttpHeaders().apply {
setContentType(MediaType.APPLICATION_JSON);
}),
Order::class.java
).addCallback(object : ListenableFutureCallback<ResponseEntity<Order>> {
override fun onSuccess(result: ResponseEntity<Order>) {
println("Result: ${result.body}")
}
override fun onFailure(ex: Throwable) {
ex.printStackTrace()
if (ex is HttpStatusCodeException) {
println(ex.responseBodyAsString)
}
}
})
I need more precise look at restTemplate at his converters, but for now you can write this part this way:
val mapper = ObjectMapper()
restTemplate.exchange(
URI.create("http://localhost:8080/foo"),
HttpMethod.PUT,
HttpEntity(mapper.writeValueAsString(Order("HELLO")), HttpHeaders().apply {
setContentType(MediaType.APPLICATION_JSON);
}),
Order::class.java
).addCallback(object : ListenableFutureCallback<ResponseEntity<Order>> {
override fun onSuccess(result: ResponseEntity<Order>) {
println("Result: ${result.body}")
}
override fun onFailure(ex: Throwable) {
ex.printStackTrace()
if (ex is HttpStatusCodeException) {
println(ex.responseBodyAsString)
}
}
})
As you see, i don't use KotlinModule and this code works perfectly, so obviously problem in cofiguration AsyncRestTemplate itself.
My 2 cents. This is surely not the solution.
I configured the asyncRestTemplate
with a AsyncHttpClientRequestInterceptor
and it magically worked. No explanation, period!
public class AsyncClientLoggingInterceptor implements AsyncClientHttpRequestInterceptor {
@Override
public ListenableFuture<ClientHttpResponse> intercept(HttpRequest request, byte[] body, AsyncClientHttpRequestExecution execution) throws IOException {
return execution.executeAsync(request, body);
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With