Patrik Mihalčin
Patrik Mihalčin

Reputation: 3991

Return ResponseEntity from Spring Integration's @Gateway method

I have IntegrationFlow where I call HTTP endpoint:

@Bean
public IntegrationFlow getInformationFlow(RestTemplate restTemplate) {
    return IntegrationFlows.from(GET_RESPONSE_ENTITY)
            .handle(Http
                    .outboundGateway(url + "/application/{code}", restTemplate)
                    .httpMethod(GET)
                    .uriVariable("code", "payload")
                    .expectedResponseType(new ParameterizedTypeReference<ResponseEntity<Information>>() {
                    })
            ).get();
}

This flow is executed when getInformation is called thanks to Gateway

@MessagingGateway
public interface MyGateway {

    @Gateway(requestChannel = GET_RESPONSE_ENTITY)
    ResponseEntity<Information> getInformation(String code);

}

Code above throws following exception:

Caused by: org.springframework.http.converter.HttpMessageConversionException: 
Type definition error: [simple type, class org.springframework.http.ResponseEntity]; 
nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: 
Cannot construct instance of `org.springframework.http.ResponseEntity` 
(no Creators, like default construct, exist): cannot deserialize from Object value (no delegate- or property-based Creator)

Jackson tries to deserialize into ResponseEntity instead of Information class

My goal is to check status code first and optionally check Information fields

Is it possible to return ResponseEntity from method annotated by @Gateway?

Upvotes: 1

Views: 1104

Answers (2)

Younes El Ouarti
Younes El Ouarti

Reputation: 2354

TL;DR: This is a new feature (thanks to OP) available in Spring Integration >=5.5.0. You need to:

  1. Get the appropriate Spring Integration version
  2. Change your MessagingGateway so it returns a ResponseEntity
  3. Configure your integration flow to not extract the body and to expect a ResponseEntity
  4. Create a new HttpMessageConverter that supports ResponseEntity

The GitHub issue/feature request that OP created, resulted in an actual feature being implemented in Spring Integration. It is now possible to receive ResponseEntity objects. What Artem Bilan explained was correct prior to the implementation of the feature and still is correct if the flag extractResponseBody is set to true (default). If it is set to false, then it is pretty easy to get a ResponseEntity (now).

What You Need To Do

First get a new version (>=5.5.0) of Spring Integration in your POM.xml or build.gradle

dependencies {

    ...
    implementation "org.springframework.integration:spring-integration-http:5.5.2"
    ...
}

Now in my case I previously had a REST call that resulted in a String. Now I want a ResponseEntity<String>. So the MessagingGateway I previously defined...

old

@MessagingGateway
public interface Client {

    /**
     * Makes call to (another) Server
     */
    @Gateway(requestChannel = "requestChannel", replyChannel = "replyChannel")
    String makeSomeCall(@Header("url") String url, @Payload SomePayload sp);
}

... becomes (only return value changed):

new

@MessagingGateway
public interface Client {

    /**
     * Makes call to (another) server
     */
    @Gateway(requestChannel = "requestChannel", replyChannel = "replyChannel")
    ResponseEntity<String> makeSomeCall(@Header("url") String url, @Payload SomePayload sp);
}

Now if we leave it this way, then it still won't work. We need to tell Spring, that the ResponseBody should not be extracted. We do this in the integration flow:


    @Bean
    MessageChannel replyChannel() {
        return MessageChannels.direct("replyChannel").get();
    }
    
    @Bean
    MessageChannel clientChannel() {
        return MessageChannels.direct().get();
    }


    IntegrationFlow clientFlow() {
        final SpelExpressionParser parser = new SpelExpressionParser();
        return IntegrationFlows.from(clientChannel())
                .handle(Http.outboundGateway(parser.parseExpression("headers.url"), restTemplate)
                     .charset("UTF-8") 
                     .extractResponseBody(false) // <-- this is new w/ default being true!
                     .expectedResponseType(ResponseEntity.class)) // <--  this was String.class
                .channel(clientReplyChannel())
                .get();
    }

If we leave it here, it will still not work. The program will not be able to find an appropriate HttpMessageConverter. You need to write one on your own. It all depends on what the content of your ResponseEntity is. Thankfully, you most likely don't need to write a whole HttpMessageConverter on your own. You can simply extend the "approriate" one and tell it to accept ResponseEntity. My new MessageConverter looks like this.

@Component
public class ResponseEntityHttpMessageConverter extends StringHttpMessageConverter {
     
    public ResponseEntityHttpMessageConverter() {
    }

    @Override
    public boolean supports(final Class<?> clazz) {
        return ResponseEntity.class == clazz;
    }

}

Spring Boot should be able to pick it up on its own. I believe there is plenty of material about this online in case you get stuck.

Hint: Just don't make the mistake to implement HttpMessageConverter<ResponseEntity>. Spring will unwrap the ResponseEntity for the MessageConverter itself. So the converter should only be for the content.

Upvotes: 0

Artem Bilan
Artem Bilan

Reputation: 121272

Short answer to your question: it depends on that MyGateway contract implementation. It is really not a gateway (or any interface API) responsibility to deal with returns. They define only contracts. It is already your goal to implement such a contract properly.

What I mean by this that Spring Integration with its EIP components does not go further than regular Java program design and architecture. It is just a particular case that this contract is an IntegrationFlow as an implementation. Therefore the problem is not with a contract, but rather implementation details, which is an HTTP call in your case.

So, better to ask the question like:

How to return a ResponseEntity<Information> from the Http.outboundGateway() with a Jackson message converter?

That's why I asked you on Gitter for this SO thread to better understand what is going on. Your original question is misleading and really has nothing to do with a @MessagingGateway. I'm even sure that is some clue in the stack trace that the problem happens on the RestTemplate call, not in the @MessagingGateway.

Now trying to help you to fix your explicit problem.

The AbstractHttpRequestExecutingMessageHandler does not return a ResponseEntity when there is a body:

protected Object getReply(ResponseEntity<?> httpResponse) {
    HttpHeaders httpHeaders = httpResponse.getHeaders();
    Map<String, Object> headers = this.headerMapper.toHeaders(httpHeaders);
    if (this.transferCookies) {
        doConvertSetCookie(headers);
    }

    AbstractIntegrationMessageBuilder<?> replyBuilder;
    MessageBuilderFactory messageBuilderFactory = getMessageBuilderFactory();
    if (httpResponse.hasBody()) {
        Object responseBody = httpResponse.getBody();
        replyBuilder = (responseBody instanceof Message<?>)
                ? messageBuilderFactory.fromMessage((Message<?>) responseBody)
                : messageBuilderFactory.withPayload(responseBody); // NOSONAR - hasBody()
    }
    else {
        replyBuilder = messageBuilderFactory.withPayload(httpResponse);
    }
    replyBuilder.setHeader(org.springframework.integration.http.HttpHeaders.STATUS_CODE,
            httpResponse.getStatusCode());
    return replyBuilder.copyHeaders(headers);
}

Only if there is no body. In that case it means there is nothing to map into a payload for the reply message, therefore we use the whole ResponseEntity.

As you see in this code snippet, the StatusCode is mapped to the org.springframework.integration.http.HttpHeaders.STATUS_CODE reply message header.

There is also some explanation in docs on the matter: https://docs.spring.io/spring-integration/docs/current/reference/html/http.html#using-cookies.

From here it means that your expectedResponseType can be only as an Information type. The RestTemplate with its HttpMessageConverts indeed doesn't know what to do with a ResponseEntity type to map.

Since it may return just an Information in the payload if it comes in the response, or may return the whole ResponseEntity with an empty body, it looks like you have to add some routing and transformation logic before returning a ResponseEntity<Information> as a reply to your @MessagingGateway call. Or you can revise that gateway contract and really implement a status code check in the integration flow via router or filter - and your @MessagingGateway consumer would be free from the HTTP stuff like status code checking and headers conversion.

Although it might not hurt to have some option on the AbstractHttpRequestExecutingMessageHandler to always return the whole ResponseEntity as a payload. Feel free to raise a GH issue a we will consider to implement it sooner than later!

Upvotes: 2

Related Questions