Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring Boot with AsyncRestTemplate Netty Client Fails

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.

The Server

@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.

The Client

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.

The Problem

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?

like image 737
Edwin Dalorzo Avatar asked Jul 12 '16 03:07

Edwin Dalorzo


2 Answers

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.

like image 185
Ruslan Avatar answered Oct 19 '22 10:10

Ruslan


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);
    }
}
like image 22
Bharath Avatar answered Oct 19 '22 10:10

Bharath