TL;DR : I tried to create a test which performs 2 CXF calls on 2 different SOAP Operations (saveUser and then getUser) but the second one fail because the SOAPAction in the http Request is set as "saveUser" instead of "getUser". I found 2 solution to overcome this problem, BUT I'm not fully satisfied of them.
Here is my Endpoint configuration :
final JaxWsProxyFactoryBean jaxWsProxyFactory = new JaxWsProxyFactoryBean();
jaxWsProxyFactory.setServiceClass(Internal.class);
jaxWsProxyFactory.setAddress(backendUrl + "INTERNAL");
jaxWsProxyFactory.setFeatures(getFeatures());
final Internal portType = (Internal) jaxWsProxyFactory.create();
((BindingProvider) portType).getRequestContext().put("thread.local.request.context", "true");
((BindingProvider) portType).getRequestContext().put("schema-validation-enabled", "false");
And here is my usage :
internalBackendG5.saveUser(getRequest);
internalBackendG5.getUser(saveRequest);
The Two operations are defined (thanks to wsdl2java) as :
(I override some packages to be more concise, code below may contains typo)
@WebMethod(action = "http://www.test.org/internal/saveUser")
@WebResult(name = "saveUserResponse", targetNamespace = "urn:test.com:xsd:internal:userpreferences", partName = "parameters")
public com.test.internal.type.SaveUserResponseType saveUser(
@WebParam(partName = "parameters", name = "saveUser", targetNamespace = "urn:test.com:xsd:internal:userpreferences")
com.test.internal.type.SaveUserRequestType parameters
);
and
@WebMethod(action = "http://www.test.org/internal/getUser")
@WebResult(name = "getUserResponse", targetNamespace = "urn:test.com:xsd:internal:userpreferences", partName = "parameters")
public com.test.internal.type.GetUserResponseType getUser(
@WebParam(partName = "parameters", name = "getUser", targetNamespace = "urn:test.com:xsd:internal:userpreferences")
com.test.internal.type.GetUserRequestType parameters
);
When I run my test, I get an error on the second call which is thrown from the backend as Soap exception :
javax.xml.ws.soap.SOAPFaultException: Unexpected element {urn:test.com:xsd:internal:userpreferences}getUser found. Expected {urn:test.com:xsd:internal:userpreferences}saveUser.
And when I look at the Soap Request received by the server, i get :
For the first request (saveUser) :
Headers: {[...] SOAPAction=["http://www.test.org/internal/saveUser"], user-agent=[Apache CXF 2.7.12]}
Payload: <soap:Envelope><soap:Body><saveUser>[...]</ns5:saveUser></soap:Body></soap:Envelope>
For the second request (getUser) :
Headers: {[...] SOAPAction=["http://www.test.org/internal/saveUser"], user-agent=[Apache CXF 2.7.12]}
Payload: <soap:Envelope><soap:Body><getUser>[...]</getUser></soap:Body></soap:Envelope>
When I launch only one of the two request, they both work.
After some research, I found that the Apache Cxf Interceptor SoapPreProtocolOutInterceptor was the responsible.
When the SoapAction string is already present in the request, it doesn't override it. And my second call (for some reasons I can't explain) reuses the Cxf Request context computed while processing the first call !
What I did to overcome that : - Write an interceptor called before SoapPreProtocolOutInterceptor that force the SoapAction deletion in the header, in order to force this interceptor to re-compute the action. - Or alternatively I can reset the request context between the 2 requests :
((BindingProvider) portType).getRequestContext().put(Header.HEADER_LIST, emptyList);
So my question is : is there a CXF bug ? (the requestContext shouldn't be shared between 2 requests) or I missed some Cxf configuration ?
PS : I Used this dependencies :
group:'org.apache.cxf', name:'cxf-rt-transports-http-jetty', version:'2.7.5'
group: 'org.apache.cxf', name: 'cxf-rt-features-clustering', version:'2.7.12'
group:'org.apache.cxf', name:'cxf-rt-frontend-jaxws', version:'2.7.12'
group: 'org.apache.cxf', name: 'cxf-api', version: '2.7.12'
group: 'org.apache.cxf', name: 'cxf-rt-bindings-soap', version: '2.7.12'
Thanks in advance !
I had the same issue and after some debugging I think I've found the cause.
I was setting my own custom http header on the PROTOCOL_HEADERS map before invoking any operations like this:
Map<String, List<String>> headers = new HashMap<>();
headers.put("header-name", Arrays.asList("value"));
proxy.getRequestContext().put(Message.PROTOCOL_HEADERS, headers);
From your example, I don't see that you are setting the PROTOCOL_HEADERS, but this was the root cause in my case, because this is the map that contains the SOAPAction header. Maybe some other part of your code might be tampering with it.
So first things first, we know the client proxy request context is not thread safe
Official JAX-WS answer: No. According to the JAX-WS spec, the client proxies are NOT thread safe. To write portable code, you should treat them as non-thread safe and synchronize access or use a pool of instances or similar.
CXF answer: CXF proxies are thread safe for MANY use cases. The exceptions are:
Use of ((BindingProvider)proxy).getRequestContext() - per JAX-WS spec, the request context is PER INSTANCE. Thus, anything set there will affect requests on other threads.
This raised some red flags for me about using this context, but it didn't seem like the reason for the problem, I was just adding a simple protocol header to it, that was not user specific.
The context is declared in the ClientImpl class and it's used as a base context for every service call:
protected Map<String, Object> currentRequestContext = new ConcurrentHashMap<String, Object>(8, 0.75f, 4);
Everytime you request the context the following method is executed:
public Map<String, Object> getRequestContext() {
if (isThreadLocalRequestContext()) {
if (!requestContext.containsKey(Thread.currentThread())) {
requestContext.put(Thread.currentThread(), new EchoContext(currentRequestContext));
}
return requestContext.get(Thread.currentThread());
}
return currentRequestContext;
}
Lets say we have not set the thread local property, so we always return the current request context, which now has our PROTOCOL_HEADERS map that still has no SOAPAction header in it.
Then comes the SoapPreProtocolOutInterceptor class, (I've extracted only the relevant parts of the setSoapAction(...) method:
Map<String, List<String>> reqHeaders
= CastUtils.cast((Map<?, ?>)message.get(Message.PROTOCOL_HEADERS));
if (reqHeaders == null) {
reqHeaders = new TreeMap<String, List<String>>(String.CASE_INSENSITIVE_ORDER);
}
if (reqHeaders.size() == 0) {
message.put(Message.PROTOCOL_HEADERS, reqHeaders);
}
if (!reqHeaders.containsKey(SoapBindingConstants.SOAP_ACTION)) {
reqHeaders.put(SoapBindingConstants.SOAP_ACTION, Collections.singletonList(action));
}
Here we're going to end up in the last if statement, when executing our first call on this client, thus setting the SOAP_ACTION header on the SAME protocol headers map, which is part of the current request context, declared in the ClientImpl class.
Any subsequent request by this client will have as a base the already populated map with protocol headers and the SOAP_ACTION header inside the context, thus resulting in the observed behavior. We'll never enter the if statement that creates a new map with PROTOCOL_HEADERS for our service call context.
SOLUTION:
The most clean solution in my opinion is to just use an interceptor to set values in your protocol headers map, note that we're not operating on the base request context at all:
public class HttpHeaderExampleInterceptor extends AbstractPhaseInterceptor
{
private static final String HEADER_NAME = "header_name";
private String value;
public HttpHeaderExampleInterceptor(String value)
{
super(Phase.MARSHAL);
this.value = value;
}
/**
* @see org.apache.cxf.binding.soap.interceptor.SoapPreProtocolOutInterceptor#setSoapAction(SoapMessage)
*/
@Override
public void handleMessage(Message message) throws Fault
{
Map<String, List<String>> protocolHeaders = CastUtils.cast((Map<?, ?>) message.get(Message.PROTOCOL_HEADERS));
if (protocolHeaders == null)
{
protocolHeaders = new TreeMap<String, List<String>>(String.CASE_INSENSITIVE_ORDER);
message.put(Message.PROTOCOL_HEADERS, protocolHeaders);
}
protocolHeaders.put(HEADER_NAME, Collections.singletonList(value));
}
}
Hope this helps, if anyone finds a mistake in my debugging please share as this issue really proved interesting.