spring-integration icon indicating copy to clipboard operation
spring-integration copied to clipboard

Support for null payloads. [INT-997]

Open spring-operator opened this issue 15 years ago • 23 comments

Oyvind Kvien opened INT-997 and commented

Spring Integration should support null payloads.

A common case is when using legacy services in a service-activator. The service might return null, and I don't want to refactor it as it's been running stable for years. If Spring Integration don't support null payloads I'll be forced the create a wrapper around it to control it's return type, which is poor design.

Another common case is when using an XmlPayloadUnmarshallingTransformer, like this: <si-xml:unmarshalling-transformer input-channel="freeTextSearchResponseChannel" unmarshaller="freeTextSearchUnmarshaller"/>

It receives XML from channel "freeTextSearchResponseChannel" and unmarshals it. If the XML is invalid the transformer will return null based on which unmarsller you pick. This will cause the message payload to be null.


Affects: 1.0.3

Reference URL: http://forum.springsource.org/showthread.php?t=85345

Issue Links:

  • #6203 Add support for null return values in gateway/service-activator methods

8 votes, 7 watchers

spring-operator avatar Feb 27 '10 06:02 spring-operator

Mark Fisher commented

This issue is a bit tricky, because in Spring Integration, we are intentionally treating NULL as a "short circuit" within a pipeline. For example, when a Filter does not accept a Message, the adapting handler implementation returns null to signify that the processing should not continue.

I understand your case, and I agree that we need to support this in some way. I would like to know what you would expect to happen when a service does return null. Would you expect it to continue through the pipeline? Or, would you expect it to immediately reply with a null-payload Message to the "replyChannel" as configured on the Message?

Each of the above options presents a challenge. For the first option, one would need to make sure that any service is handling potential null values, whereas now that is avoided at the framework level. For the second option, it might be too early to send a reply in some cases, e.g. a splitter->[service-activators]->aggregator where a null return occurs in one of the intermediate services (should wait until the aggregator replies).

spring-operator avatar Feb 28 '10 01:02 spring-operator

Oyvind Kvien commented

I'd expect it to continue through the pipeline. This will give you the opportunity for further processing of the message even though the payload is null.

spring-operator avatar Feb 28 '10 17:02 spring-operator

Oleg Zhurakousky commented

I think the 'null' behavior that we currently have in SI should remain untouched for one simple reason; A 'null' in itself is a cause of many issues (e.g., NPE etc...) and relying on it is not reliable ;) One of the options we might consider is creating a NullPayload object as the way to signal that this message doe not have a payload, but it is also not an accident.

spring-operator avatar Feb 28 '10 20:02 spring-operator

Oyvind Kvien commented

Yeah a NullPayload object like in i.e. Mule would be satisfactory.

spring-operator avatar Feb 28 '10 23:02 spring-operator

Mark Fisher commented

The main question is how should we expect downstream components to handle this payload? In some cases, you might still want to do something (e.g. reading or writing headers), but it seems like in most cases, the various handlers would not know how to deal with the payload.

Rather than starting from an implementation detail (like handling null as a payload), can we step back and talk about the use-case scenario a bit?

spring-operator avatar Mar 01 '10 10:03 spring-operator

Oleg Zhurakousky commented

Mark this came up several times in the forums and few engagements. For example:

Gateway:

public void initiateEvent(@Header("eventType") String event)

In this case the Message won't have any payload, but it is anything but empty, and has plenty of valuable information.

  1. method name available with Message History
  2. custom header (eventType) etc...

The goal is to use the method invocation as an event and the way to initiate an integration flow. So in a way it is a perfectly valid message.

spring-operator avatar Mar 02 '10 02:03 spring-operator

Mark Fisher commented

I see, but in the "traditional" event model an Event object would contain minimally the "source". Therefore, when translating that model to messaging, I would say that an EventMessage should have a source as its payload.

By that token, if a Service accepts a DocumentMessage but returns null, the response message could be an EventMessage.

Perhaps, I'm just overreacting, but "NullPayload" has always seemed like a hack to me. I think a cleaner solution is possible... just not yet sure what it is ;) So, please continue with the use-case samples.

spring-operator avatar Mar 02 '10 02:03 spring-operator

Oyvind Kvien commented

Just a little update about how we decided to solve the null payload issue in my current project.

The initial problem was that the executing thread would hang when a null payload was returned from a <service-activator>. It's a very serious problem that could easily bring down the server.

ServiceActivatingHandler has a requiresReply property that can make the handler throw a MessageHandlingException if the service returns null. The default of this property is false, and it is unfortunately not exposed in the Spring Integration namespace support (<service-activator>). It would be really easy to write an extended namespace (myapp:service-activator) where this property is true by default. If all usages are then switched from <service-activator> to myapp:service-activator we would then ensure that the problem with the hanging threads resulting from a null payload would never occur. It's also very easy to extend the namespace with custom handling. In my project we typically want to throw an EntityNotFoundException.

Another solution we thought about was configuring a channel on a global scope where all messages with a null payload should be sent to. But then again we would need some kind of component to somehow interprit the message for further processing.

Just food for thought.

spring-operator avatar Aug 31 '10 03:08 spring-operator

Mark Fisher commented

Good news! We have already added the "requires-reply" attribute to the <service-activator> element in 2.0 (see #5381).

I've also created an issue to address that for 1.0.5: #5388

The behavior is such that requires-reply=TRUE will trigger a MessageHandlingException will be thrown when the underlying service method returns null. The default value is still FALSE.

@Oyvind: In your opinion, does this new configuration option supersede the current JIRA issue? I am inclined to resolve this accordingly.

Thanks for the feedback, Mark

spring-operator avatar Aug 31 '10 08:08 spring-operator

Oyvind Kvien commented

Well, yes the requires-reply attribute does solve the initial problem. But just throwing a MessageHandlingException provides little flexibility to give any kind of meaning to why the payload is null. At least in my project when a <service-activator> returns null I usually want to throw a given exception to be caught at a higher level.

What would be nice is the possibility to also configure a reference to a optional handler that's invoked when payload is null.

I.e: <service-activator null-handler-ref="myNullHandler"/>

It would at least solve my problems pretty nicely.

spring-operator avatar Aug 31 '10 13:08 spring-operator

Mark Fisher commented

I have thought about this as well.

The first thing I'd like to ask is why you can't create a simple wrapper object that invokes the real target service and handles Exception-throwing/wrapping or better yet apply an AOP proxy with after/after-throwing advice?

Secondly, what is upstream from the <service-activator>? If you have a <gateway> upstream, we do support an "exception-mapper" now.

spring-operator avatar Aug 31 '10 14:08 spring-operator

Oyvind Kvien commented

We did use wrapper objects to begin with, but decided to go for a more generic solution with an extended namespace as mentioned above.

The problem isn't how to get around the null payload issue. I guess the main problem is, as #5381 says, the gateway method will hang until it times out when the ServiceActivator returns a null payload downstream from a gateway. This is very dangerous behaviour and as far as I know not documented. Isn't that a really bug?

I guess what I'm trying to say is that unless this behaviour is changed, the ServiceActivator need to explicitly document or handle this in some good/generic way.

spring-operator avatar Sep 01 '10 00:09 spring-operator

Christian Nedregård commented

Hi.

I work with Øyvind.

We are seeing similar behaviour also when using splitter/aggregator on a direct channel. If the original payload is an empty list the current thread will be hanging forever.

This behaviour is unexpected and, as mentioned above, dangerous. It can take down a server in seconds if it receives many requests that generates this type of payload.

It seems unsatisfactory to solve this by adding special configuration to specific endpoints. Its easy to forget and the issue could be re-introduced with new endpoints in the future.

Is there a global way I can configure my channels so that they will never hang on a null payload?

Regards Christian

spring-operator avatar Dec 15 '10 06:12 spring-operator

Mark Fisher commented

Christian, What is the entry point to your message flow?, and are you using 2.0 now? The best way to avoid this through a single configuration is to set the timeout on the entry point.

spring-operator avatar Dec 15 '10 07:12 spring-operator

Christian Nedregård commented

Thanks for your quick reply Mark :)

We are upgrading to 2.0 early next year.

All our entry points are through GatewayProxyFactoryBean via a specialized gateway namespace handler. We'll set a default timeout there then. Thanks for the tip.

This will prevent threads from hanging forever. Some of our channels will require a quite large timeout though (e.g. 200 seconds) so they will still make the whole servert vulnerable for thread starvation.

IMHO the timeout is still just a temporary fix though. Are hanging threads really the expected and acceptable behavior of a channel that receives a null payload from an endpoint? I realize that our usage of SI might be more serial than the regular users, but still.. this does not make sense to me.

Is this issue still under consideration?

Thanks again

Chr

spring-operator avatar Dec 15 '10 08:12 spring-operator

Mark Fisher commented

The concept of "hanging" is really only relevant if there is some process waiting for a reply. The way that Spring Integration is designed, components can return NULL to indicate "nothing left to do" in a flow (e.g. a Message Filter that does not accept a Message to be processed, and has no discard-channel). In other words, if you consider a one-way, fire-and-forget flow, NULL simply means the flow stops there. When, however, you are using a Gateway as an entry point, that Gateway might be expecting a reply. It is up to the particular end user, based on their knowledge/expectations, to determine how long of a timeout makes sense in their case (we considered a default value other than "indefinite", but decided that any other value would be arbitrary; there is no one-size-fits-all setting, so we have an indefinite blocking wait as the default (just like many blocking components within Java - such as BlockingQueue - where you either provide a timeout or have an indefinite wait). That said, if you happen to know that your downstream components are going to be invoked in a flow that is initiated by a gateway that will be expecting a reply, then you can configure certain settings. For example, you can set requires-reply="true" on a service-activator, so that it will throw an Exception immediately if a NULL value is returned. Likewise, you can specify that an Exception should be thrown if a router fails to resolve a channel or if a filter rejects a Message. The reason we do not throw Exceptions by default is that the NULL-as-end-of-flow case is completely valid and useful for one-way flows or even request/reply flows where you choose to allow the NULL return option but provide a reasonable timeout value based on your expectations about the flow. In general, for timeouts as well as error-handling, transactions, etc., we promote the idea of considering "entry points" and flow initiators as critical to such configuration.

Does that help make sense of our rationale behind the default behavior?

spring-operator avatar Dec 15 '10 09:12 spring-operator

Christian Nedregård commented

Yes. Thanks for explaining this so thoroughly. We'll have a look at our flows, and set realistic default timeouts on their entry points where needed.

Cheers Chr

spring-operator avatar Dec 15 '10 15:12 spring-operator

Oleg Zhurakousky commented

Christian

I would also suggest to read-up on Asynchronous Gateways (the new feature of Spring Integration 2.0) which provides another approach for handling scenarios where reply can not always be guaranteed. You can read more about it here: http://static.springsource.org/spring-integration/reference/htmlsingle/#async-gateway

We also have a working sample showing it. You can get it from the sample repo

spring-operator avatar Dec 15 '10 15:12 spring-operator

fabio gomes commented

Guys,

When the SOAP response has an empty soap body (A),

<S:Envelope xmlns:S="http://schemas.xmlsoap.org/soap/envelope/">
   <S:Body/>
</S:Envelope>

the Spring Integration MessageBuilder raise an exception, because the empty body (A) is mapped to a null payload.

Note 1: See the method org.springframework.integration.ws.AbstractWebServiceOutboundGateway.ResponseMessageExtractor.extractData.

It seems that re-writing the empty soap message, adding the element "<null/>" (or other values, because apparently it does not matter) in the soap body avoid the null payload problem and does not introduce side effects (lucky guess). Therefore, instead of (A), I can write (B), showed bellow.

<S:Envelope xmlns:S="http://schemas.xmlsoap.org/soap/envelope/">
   <S:Body>
     <null/>
   </S:Body>
</S:Envelope>

Note 2: If you need to check if the original message body is empty, you can verify the presence of the element "<null/>" in the SOAP body.

I used this strategy to write a workaround for the null payload problem.

An aspectJ rewrites the AbstractWebServiceOutboundGateway.ResponseMessageExtractor.extractData. When WebServiceMessage.getPayloadSource() is null, the aspect returns the string "<null/>", instead of null. Otherwise, it call the original implementation, pjp.proceed(). See the (C) painel bellow.

Note 3: This workaround does not interfere in non soap messages.

@Aspect
public class AvoidNullSoapMessageAspect {
	
	@Around("responseMessageExtractorExtractDataPointcut()")
	public Object extractData(final ProceedingJoinPoint pjp) throws Throwable{
		
		final Object[] args = pjp.getArgs();
		final WebServiceMessage message = (WebServiceMessage) args[0];
		
		if (message instanceof SoapMessage) {
			if (message.getPayloadSource() == null) {
				return "<null/>";   //<-- workaround
			} else{
				return pjp.proceed();
			}
		} else{
			return pjp.proceed();
		}
	}
	
	@Pointcut("execution(Object org.springframework.integration.ws.AbstractWebServiceOutboundGateway.ResponseMessageExtractor.extractData(org.springframework.ws.WebServiceMessage))")
	public void responseMessageExtractorExtractDataPointcut() {};
}

I have successfully tested this workaround in many web service, SOAP version 1.1 (I have not tested it in SOAP version 1.2).

spring-operator avatar Mar 22 '12 11:03 spring-operator

Oleg Zhurakousky commented

"Spring Integration MessageBuilder raise an exception, because the empty body (A) is mapped to a null payload."

I think one of the things we may consider is map empty response to empty string - "", because after all it is a response which is empty

spring-operator avatar Mar 23 '12 06:03 spring-operator

Artem Bilan commented

Some SO question with similar background: http://stackoverflow.com/questions/43524745/spring-integration-service-activator-not-sending-null-reply

spring-operator avatar Apr 20 '17 18:04 spring-operator

Artem Bilan commented

We are going to revise this in the nearest future probably as an Optional (or NullMessage according to Null Object Pattern) intermediary which is going to be resolved to null for POJO method arguments where the payload is expected. Transferring such an Optional over the network might lead to converting it to empty string or new byte[0] according to the target protocol expectations since many of them just don't support null payload as Spring Integration does at the moment.

spring-operator avatar Dec 01 '22 21:12 spring-operator

The related Spring Framework issue regarding method argument resolver has been fixed: https://github.com/spring-projects/spring-framework/issues/28945 Now we can produce a message with Optional.empty() and it is going to be handled in POJO method invoker properly.

artembilan avatar Feb 13 '23 18:02 artembilan