0
votes

I have a message flow where I'd like to filter/drop the message if the result of a GET HTTP call (via http:outbound-gateway) is statusCode 200 - i.e case already exists. Put another way if the call gets a 404 (not found) the flow should continue. Ideally any other status codes or exceptions should go to errorHandler (as now)

I've tried calling a gateway that uses a chain with http:outbound-gateway with a request-handler-advice-chain, thinking I can trap the 404 and then just let things continue as if it was not an error, then test the statusCode 404 in 'filter'. However hitting a few issues

  1. The onFailureExpression expression doesn't detect statusCode 404, log shows HttpClientErrorException so presumably exception wasn't caught ?
  2. What value should I return from onFailureExpression ? null or payload or #payload ? ideally I want the payload before the HTTP call to remain as is.
  3. Do I need trapException true ?
  4. Surprised there isn't an easier way ? shame can't just declare on http:outbound-gateway httpStatusCodes that are allowed and treated as normal. I didn't think an errorChannel and error handler was the right way to go as I want the original flow to continue if 404 status code. So looks odd to put wanted regular behaviour in an error flow, and already have a higher level error handler.
2020-11-24 10:46:45,060 [main] DEBUG org.springframework.web.client.RestTemplate - GET request for "http://localhost:9095/ccacase/V1/case/challenge/CHG123456789" resulted in 404 (Not Found); invoking error handler
2020-11-24 10:46:45,134 [main] DEBUG org.springframework.integration.channel.DirectChannel - postSend (sent=true) on channel 'getChallengeCaseChannel', message: GenericMessage [payload=uk.gov.voa.integration.ccacasecheck.json.CdbEvent@624b3544[id=22,eventType=MIGRATE_CHALLENGE_CASE,asstRef=<null>,ccaCaseRef=CHG123456789,assessmentStatus=<null>,settlementCode=<null>,eventDateTime=2020-11-23T12:05,uarn=<null>,sourceActivityId=<null>,activityAction=<null>,additionalProperties={}], headers={replyChannel=org.springframework.messaging.core.GenericMessagingTemplate$TemporaryReplyChannel@54f6b629, errorChannel=org.springframework.messaging.core.GenericMessagingTemplate$TemporaryReplyChannel@54f6b629, originalPayload=uk.gov.voa.integration.ccacasecheck.json.CdbEvent@624b3544[id=22,eventType=MIGRATE_CHALLENGE_CASE,asstRef=<null>,ccaCaseRef=CHG123456789,assessmentStatus=<null>,settlementCode=<null>,eventDateTime=2020-11-23T12:05,uarn=<null>,sourceActivityId=<null>,activityAction=<null>,additionalProperties={}], message_id=22, history=cdbEventsAMQPChannel,routeEventChain,migrateChallengeCaseEventChannel,migrateChallengeCaseEventChain,statusFlow,getChallengeCaseChannel, id=6d99e890-fc56-74a8-1187-fbc3457685c8, timestamp=1606214804576}]
2020-11-24 10:46:45,135 [main] DEBUG org.springframework.integration.handler.ExpressionEvaluatingMessageProcessor - SpEL Expression evaluation failed with Exception.org.springframework.web.client.HttpClientErrorException: 404 Not Found


Caused by: org.springframework.messaging.MessagingException: failed to transform message headers; nested exception is org.springframework.messaging.MessageHandlingException: Expression evaluation failed: @statusFlow.exchange(#root).headers[http_statusCode]; nested exception is org.springframework.web.client.HttpClientErrorException: 404 Not Found
    at org.springframework.integration.transformer.HeaderEnricher.transform(HeaderEnricher.java:128)
    at org.springframework.integration.transformer.MessageTransformingHandler.handleRequestMessage(MessageTransformingHandler.java:89)
    ... 72 more
Caused by: org.springframework.messaging.MessageHandlingException: Expression evaluation failed: @statusFlow.exchange(#root).headers[http_statusCode]; nested exception is org.springframework.web.client.HttpClientErrorException: 404 Not Found
    at org.springframework.integration.util.AbstractExpressionEvaluator.evaluateExpression(AbstractExpressionEvaluator.java:143)
    at org.springframework.integration.handler.ExpressionEvaluatingMessageProcessor.processMessage(ExpressionEvaluatingMessageProcessor.java:72)
    at org.springframework.integration.transformer.support.ExpressionEvaluatingHeaderValueMessageProcessor.processMessage(ExpressionEvaluatingHeaderValueMessageProcessor.java:71)
    at org.springframework.integration.transformer.HeaderEnricher.transform(HeaderEnricher.java:119)
    ... 73 more
Caused by: org.springframework.web.client.HttpClientErrorException: 404 Not Found
    at org.springframework.web.client.DefaultResponseErrorHandler.handleError(DefaultResponseErrorHandler.java:91)
<int:chain id="migrateChallengeCaseEventChain" input-channel="migrateChallengeCaseEventChannel">

        <int:header-enricher>                                
            <int:header name="caseStatusCode" expression="@statusFlow.exchange(#root).headers[http_statusCode]" />    
        </int:header-enricher>
        
        <!-- to check http_statusCode header and drop any message with found 200 statusCode -->
        <int:filter ref="status200Filter"/>  
        
        <int:transformer ref="migrateChallengeCaseTransformer" />
        <int:transformer ref="jsonValidationTransformer" />
        <int:object-to-json-transformer object-mapper="springJacksonObjectMapper" />
        <int:header-enricher>
             <int:header name="contentType" value="application/json;charset=UTF-8" overwrite="true"/>
        </int:header-enricher>  
        <int-amqp:outbound-channel-adapter 
            amqp-template="amqpTemplate" exchange-name="cdbEvents.exchange" 
            routing-key="migrateCdbCcaChallengeCase.request.queue.binding" />
    </int:chain>    
    
    <int:gateway id="statusFlow" default-request-channel="getChallengeCaseChannel" />  
    
    <int:channel id="getChallengeCaseChannel" />
    
    <!-- Call API to see if Case already exists, 200 status code we want to filter/drop message   -->
    <int:chain id="getChallengeCaseChain" input-channel="getChallengeCaseChannel">
        <int-http:outbound-gateway id="httpOutboundGatewayChallengeCaseGet"     
                            expected-response-type="java.lang.String"                                           
                            http-method="GET"   charset="UTF-8"
                            extract-request-payload="true"  
                            request-factory="httpRequestFactory"                    
                            url="${mule.case.data.service.uri}/${case.challenge.subpath}/{ccaCaseRefValue}">
            <int-http:uri-variable name="ccaCaseRefValue" expression="headers['originalPayload'].ccaCaseRef"/>  
            <int-http:request-handler-advice-chain>
                    <bean class="org.springframework.integration.handler.advice.ExpressionEvaluatingRequestHandlerAdvice">
                        <!-- If true, the result of evaluating the onFailureExpression will be returned as the result -->
                        <property name="returnFailureExpressionResult" value="true" />
                        <property name="onSuccessExpression" value="payload" />
                        <!-- If true, any exception will be caught and null returned. Default false -->
                         <property name="trapException" value="true" /> 
                        <!-- Set the expression to evaluate against the root message after a failed handler invocation. The exception is available as the variable #exception. Defaults to payload, if failureChannel is configured. -->
                        <property name="onFailureExpression"  value="#exception.cause.statusCode == 404 ? payload : #exception"/> 
                        <!-- Set the channel name to which to send the ErrorMessage after evaluating the failure expression. -->
                        <!-- <property name="failureChannel" ref="#headers['replyChannel']" />  -->
                    </bean>
                </int-http:request-handler-advice-chain>
        </int-http:outbound-gateway>
    
    </int:chain>    

Solution used was to implement a custom ErrorHandler that extends the default and overrides hasError

/**
 * To be used with an 'int-http:outbound-gateway' to make a HTTP call and allow a statusCode 404 
 * response to be treated as normal (as well as a 200 statusCode). 
 * Normally a statusCode 404 would throw a 
 * org.springframework.web.client.HttpClientErrorException: 404 Not Found
 * and expect the caller to deal with an error-channel.  
 * This allows processing to continue and subsequently test for a 404 response
 * and route differently 
 *
 */
public class Allow404StatusCodeResponseHandler extends DefaultResponseErrorHandler  {

    /**
     * OVERRIDEN 
     * 
     * Indicate whether the given response has any errors.
     * <p>Implementations will typically inspect the
     * {@link ClientHttpResponse#getStatusCode() HttpStatus} of the response.
     * @param response the response to inspect
     * @return {@code true} if the response indicates an error; {@code false} otherwise
     * @throws IOException in case of I/O errors
     */
    @Override
    public boolean hasError(ClientHttpResponse response) throws IOException {
        int rawStatusCode = response.getRawStatusCode();
        return (rawStatusCode == 404 || rawStatusCode == 200) ? false : true;  // 404 to be treated as normal and testable downstream
    }

}

Then set this in int-http:outbound-gateway

1

1 Answers

0
votes

The <int-http:outbound-gateway> is fully based on the RestTemplate from Spring Web. And that one comes with the DefaultResponseErrorHandler, which really treats 4xx as an error - the standard HTTP protocol behavior: https://www.restapitutorial.com/httpstatuscodes.html#.

Also see that class JavaDocs:

/**
 * Spring's default implementation of the {@link ResponseErrorHandler} interface.
 *
 * <p>This error handler checks for the status code on the
 * {@link ClientHttpResponse}. Any code in the 4xx or 5xx series is considered
 * to be an error. This behavior can be changed by overriding
 * {@link #hasError(HttpStatus)}. Unknown status codes will be ignored by
 * {@link #hasError(ClientHttpResponse)}.
 *
 * <p>See {@link #handleError(ClientHttpResponse)} for more details on specific
 * exception types.
 *
 * @author Arjen Poutsma
 * @author Rossen Stoyanchev
 * @author Juergen Hoeller
 * @since 3.0
 * @see RestTemplate#setErrorHandler
 */
public class DefaultResponseErrorHandler implements ResponseErrorHandler {

Yes, you can set any custom ResponseErrorHandler into that <int-http:outbound-gateway> to suppress a default behavior, but I still think it is better to deal with the ExpressionEvaluatingRequestHandlerAdvice to handle that HttpClientErrorException and its getStatusCode(). Yes, you probably need to set the trapException to true to avoid re-throw of that HttpClientErrorException. And yes in the failure flow you have an access to the original message before request. See ErrorMessage, the MessagingException as its payload and the failedMessage property.

You probably may try to implement your own AbstractRequestHandlerAdvice with much simpler, your use-case specific logic: ignore 200 and handle 404 appropriately: https://docs.spring.io/spring-integration/docs/current/reference/html/messaging-endpoints.html#message-handler-advice-chain

The status code 200 is going to be present in the HttpHeaders.STATUS_CODE header of the reply AbstractIntegrationMessageBuilder for your consideration in the advice.