Search code examples
spring-bootspring-cloudamazon-sqs

SQS Listener @Headers getting body content instead of Message Attributes


I am using Spring Cloud SQS messaging for listening to a specified queue. Hence using @SqsListener annotation as below:

    @SqsListener(value = "${QUEUE}", deletionPolicy = SqsMessageDeletionPolicy.ALWAYS )
    public void receive(@Headers Map<String, String> header, @Payload String message)  {
        try {
            logger.logInfo("Message payload is: "+message);
            logger.logInfo("Header from SQS is: "+header);

            if(<Some condition>){
                //Dequeue the message once message is processed successfully
                awsSQSAsync.deleteMessage(header.get(LOOKUP_DESTINATION), header.get(RECEIPT_HANDLE));
            }else{
                logger.logInfo("Message with header: " + header + " FAILED to process");
                logger.logError(FLEX_TH_SQS001);
            }
        } catch (Exception e) {
            logger.logError(FLEX_TH_SQS001, e);
        }       
    }

I am able to connect the specified queue successfully and read the message as well. I am setting a message attribute as "Key1" = "Value1" along with message in aws console before sending the message. Following is the message body:

{
"service": "ecsservice"
}

I am expecting "header" to receive a Map of all the message attributes along with the one i.e. Key1 and Value1. But what I am receiving is: {service=ecsservice} as the populated map.

That means payload/body of message is coming as part of header, although body is coming correctly.

I wonder what mistake I am doing due to which @Header header is not getting correct message attributes.

Seeking expert advice.

-PC


Solution

  • I faced the same issue in one of my spring projects. The issue for me was, SQS configuration of QueueMessageHandlerFactory with Setting setArgumentResolvers.

    By default, the first argument resolver in spring is PayloadArgumentResolver. with following behavior

    @Override
        public boolean supportsParameter(MethodParameter parameter) {
            return (parameter.hasParameterAnnotation(Payload.class) || this.useDefaultResolution);
        }
    

    Here, this.useDefaultResolution is by default set to true – which means any parameter can be converted to Payload.

    And Spring tries to match your method actual parameters with one of the resolvers, (first is PayloadArgumentResolver) - Indeed it will try to convert all the parameters to Payload.

    Source code from Spring:

    @Nullable
        private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
            HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
            if (result == null) {
                for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) {
                    if (resolver.supportsParameter(parameter)) {
                        result = resolver;
                        this.argumentResolverCache.put(parameter, result);
                        break;
                    }
                }
            }
            return result;
        }
    

    How I solved this,

    The overriding default behavior of Spring resolver

    factory.setArgumentResolvers(
                listOf(
                    new PayloadArgumentResolver(converter, null, false),
                    new HeaderMethodArgumentResolver(null, null)
                )
            )
    

    Where I set, default flag to false and spring will try to convert to payload only if there is annotation on parameter.

    Hope this will help.