How to return custom SOAP Error from Spring Boot Endpoint Service?

Kosi2801 picture Kosi2801 · Dec 7, 2018 · Viewed 7.1k times · Source

I have set up an Webservice application which receives and just logs SOAP requests from a third party. After logging a defined response has to be returned. This works without problems if there are no errors and the received SOAP requests match the WSDL. Unfortunately the third party also expects a proper SOAP response when it sends invalid content or even random data.

In case the request contains random data (eg "zewrzasjkfklj") my Service returns a HTTP/400 Bad Request with an empty body. In case the request containx XML but NOT Soap (eg. "") the Service returns a HTTP/500 Server Error with a JSON Body

{"timestamp":"2018-12-06T16:16:29.375+0000","status":500,"error":"Internal Server Error","message":"Could not create message from InputStream: Unable to create envelope from given source: ; nested exception is com.sun.xml.internal.messaging.saaj.SOAPExceptionImpl: Unable to create envelope from given source: ","path":"/NotificationServicePort"}

This is especially confusing to me as I have no trace or configuration anywhere in the project relating to JSON.

The endpoint is a class annotated with @Endpoint which implements

...    @PayloadRoot(namespace = NAMESPACE_URI, localPart = "notify")
    @ResponsePayload
    public JAXBElement<NotifyResponse> notify(@RequestPayload Notify request) {
...}

(but this method is never reached in case of the invalid requests).

I already tried to implement/provide Interceptors, Dispatchers, ErrorMappers, ... but the results did not change. It seems that in the latter case (valid XML but no SOAP) it fails when attempting to extract the Envelope at SOAPPartImpl.lookForEnvelope() and fails with a throw new SOAPExceptionImpl("Unable to create envelope from given source because the root element is not named \"Envelope\""); A breakpoint at that error gives the following stack:

lookForEnvelope:153, SOAPPartImpl (com.sun.xml.internal.messaging.saaj.soap)
getEnvelope:121, SOAPPartImpl (com.sun.xml.internal.messaging.saaj.soap)
createEnvelope:110, EnvelopeFactory (com.sun.xml.internal.messaging.saaj.soap)
createEnvelopeFromSource:69, SOAPPart1_1Impl (com.sun.xml.internal.messaging.saaj.soap.ver1_1)
getEnvelope:128, SOAPPartImpl (com.sun.xml.internal.messaging.saaj.soap)
createWebServiceMessage:189, SaajSoapMessageFactory (org.springframework.ws.soap.saaj)
createWebServiceMessage:60, SaajSoapMessageFactory (org.springframework.ws.soap.saaj)
receive:92, AbstractWebServiceConnection (org.springframework.ws.transport)
handleConnection:87, WebServiceMessageReceiverObjectSupport (org.springframework.ws.transport.support)
handle:61, WebServiceMessageReceiverHandlerAdapter (org.springframework.ws.transport.http)
doService:293, MessageDispatcherServlet (org.springframework.ws.transport.http)
processRequest:974, FrameworkServlet (org.springframework.web.servlet)
doPost:877, FrameworkServlet (org.springframework.web.servlet)
service:661, HttpServlet (javax.servlet.http)
service:851, FrameworkServlet (org.springframework.web.servlet)
service:742, HttpServlet (javax.servlet.http)
internalDoFilter:231, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilter:52, WsFilter (org.apache.tomcat.websocket.server)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:246, AbstractRequestLoggingFilter (org.springframework.web.filter)
doFilter:107, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
filterAndRecordMetrics:158, WebMvcMetricsFilter (org.springframework.boot.actuate.metrics.web.servlet)
filterAndRecordMetrics:126, WebMvcMetricsFilter (org.springframework.boot.actuate.metrics.web.servlet)
doFilterInternal:111, WebMvcMetricsFilter (org.springframework.boot.actuate.metrics.web.servlet)
doFilter:107, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:90, HttpTraceFilter (org.springframework.boot.actuate.web.trace.servlet)
doFilter:107, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilter:320, FilterChainProxy$VirtualFilterChain (org.springframework.security.web)
invoke:127, FilterSecurityInterceptor (org.springframework.security.web.access.intercept)
doFilter:91, FilterSecurityInterceptor (org.springframework.security.web.access.intercept)
doFilter:334, FilterChainProxy$VirtualFilterChain (org.springframework.security.web)
doFilter:119, ExceptionTranslationFilter (org.springframework.security.web.access)
doFilter:334, FilterChainProxy$VirtualFilterChain (org.springframework.security.web)
doFilter:137, SessionManagementFilter (org.springframework.security.web.session)
doFilter:334, FilterChainProxy$VirtualFilterChain (org.springframework.security.web)
doFilter:111, AnonymousAuthenticationFilter (org.springframework.security.web.authentication)
doFilter:334, FilterChainProxy$VirtualFilterChain (org.springframework.security.web)
doFilter:170, SecurityContextHolderAwareRequestFilter (org.springframework.security.web.servletapi)
doFilter:334, FilterChainProxy$VirtualFilterChain (org.springframework.security.web)
doFilter:63, RequestCacheAwareFilter (org.springframework.security.web.savedrequest)
doFilter:334, FilterChainProxy$VirtualFilterChain (org.springframework.security.web)
doFilterInternal:158, BasicAuthenticationFilter (org.springframework.security.web.authentication.www)
doFilter:107, OncePerRequestFilter (org.springframework.web.filter)
doFilter:334, FilterChainProxy$VirtualFilterChain (org.springframework.security.web)
doFilter:116, LogoutFilter (org.springframework.security.web.authentication.logout)
doFilter:334, FilterChainProxy$VirtualFilterChain (org.springframework.security.web)
doFilterInternal:66, HeaderWriterFilter (org.springframework.security.web.header)
doFilter:107, OncePerRequestFilter (org.springframework.web.filter)
doFilter:334, FilterChainProxy$VirtualFilterChain (org.springframework.security.web)
doFilter:105, SecurityContextPersistenceFilter (org.springframework.security.web.context)
doFilter:334, FilterChainProxy$VirtualFilterChain (org.springframework.security.web)
doFilterInternal:56, WebAsyncManagerIntegrationFilter (org.springframework.security.web.context.request.async)
doFilter:107, OncePerRequestFilter (org.springframework.web.filter)
doFilter:334, FilterChainProxy$VirtualFilterChain (org.springframework.security.web)
doFilterInternal:215, FilterChainProxy (org.springframework.security.web)
doFilter:178, FilterChainProxy (org.springframework.security.web)
invokeDelegate:357, DelegatingFilterProxy (org.springframework.web.filter)
doFilter:270, DelegatingFilterProxy (org.springframework.web.filter)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:99, RequestContextFilter (org.springframework.web.filter)
doFilter:107, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:109, HttpPutFormContentFilter (org.springframework.web.filter)
doFilter:107, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:93, HiddenHttpMethodFilter (org.springframework.web.filter)
doFilter:107, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:200, CharacterEncodingFilter (org.springframework.web.filter)
doFilter:107, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:193, ApplicationFilterChain (org.apache.catalina.core)
doFilter:166, ApplicationFilterChain (org.apache.catalina.core)
invoke:198, StandardWrapperValve (org.apache.catalina.core)
invoke:96, StandardContextValve (org.apache.catalina.core)
invoke:496, AuthenticatorBase (org.apache.catalina.authenticator)
invoke:140, StandardHostValve (org.apache.catalina.core)
invoke:81, ErrorReportValve (org.apache.catalina.valves)
invoke:87, StandardEngineValve (org.apache.catalina.core)
service:342, CoyoteAdapter (org.apache.catalina.connector)
service:803, Http11Processor (org.apache.coyote.http11)
process:66, AbstractProcessorLight (org.apache.coyote)
process:790, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1468, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:49, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1149, ThreadPoolExecutor (java.util.concurrent)
run:624, ThreadPoolExecutor$Worker (java.util.concurrent)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:748, Thread (java.lang)

I would be grateful for any hints or further suggestions where I can find more information on how to set up a default SOAP response (or a HTML response which has text contents of a SOAP message) if the request did not even reach the SOAP processing logic.

Answer

Kosi2801 picture Kosi2801 · Dec 12, 2018

In the end it had to be a combination of several hooks, as there seems to be no single spot or configuration available where all errors/issues related to an endpoint pass AND allow a custom response to be generated.

Following was my solution I came up with eventually:

The main location where I could bundle the generation of the customized response is a customized MessageDispatcherServlet:

...
// this custom dispatcher is responsible for sending back a faked "SOAP" like response upon any type of
// misformatted request or error.
@Component
public class CustomSoapErrorMessageDispatcherServlet extends MessageDispatcherServlet {

    @Override
    protected void doService(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse)
            throws Exception {
        Exception thrownException = null;

        try {
            super.doService(httpServletRequest, httpServletResponse);
        } catch (CustomSoapValidationException | SoapMessageCreationException e) {
            LOG.warn("Processing resulted in exception: " + e.getMessage()); //
            thrownException = e;
            httpServletResponse.setStatus(400);
        } catch (Exception e) {
            LOG.warn("Processing resulted in generic exception: " + e.getMessage()); //
            thrownException = e;
            httpServletResponse.setStatus(500);
        }

        int responseStatus = httpServletResponse.getStatus();

        // Response in HTTP OK Range? Do nothing.
        if (responseStatus >= 200 && responseStatus <= 299) {
            return;
        }

        /*
        In any case of any error send a SOAP-like response. 
         */
        String errorCode, errorMessage;

        // failure during SOAP interpretion? ie. XML received but not SOAP or invalid structure, ....
        if(thrownException instanceof SoapMessageException) {
            errorCode = "110";
            errorMessage = "Generic SOAP Exception: " + thrownException.getMessage();
        }
        // did our structure validation fail?
        else if (thrownException instanceof CustomSoapValidationException) {
            errorCode = "110";
            errorMessage = "Structure error in request: " + thrownException.getMessage();
        }
        // another exception unrelated to Soap Processing?
        else if (thrownException != null) {
            errorCode = "999";
            errorMessage = "Internal error: " + thrownException.getMessage();
        }
        // generic internal error, but not throwing exception?
        else if (responseStatus >= 400 && responseStatus <= 499) {
            errorCode = String.valueOf(responseStatus);
            errorMessage = "Generic unspecific request processing error.";
        }
        // something completely unexpected
        else {
            errorCode = "500";
            errorMessage = "Unexpected condition.";
        }

        String responseBody = generateSoapErrorContent(errorCode, errorMessage);
        ServletOutputStream outputStream = httpServletResponse.getOutputStream();
        outputStream.print(responseBody);
        outputStream.flush();
    }   
    ...
}
...

which I activated via my configuration class with

...
    @Autowired
    private CustomSoapErrorMessageDispatcherServlet dispatcherServlet;

    @Bean
    public ServletRegistrationBean messageDispatcherServlet(ApplicationContext applicationContext) {
        dispatcherServlet.setApplicationContext(applicationContext);
        dispatcherServlet.setTransformWsdlLocations(true);
        return new ServletRegistrationBean(dispatcherServlet, "/NotificationServicePort/*");
    }
...

This custom dispatcher alone would just be able to catch requests which contain (valid and invalid) XML but not exactly SOAP or requests containing random data. To also cover non-valid SOAP requests some more steps were necessary.

Firstly a custom interceptor which performs the schema validation and throws a custom exception (instead of immediately respondingn with a SOAP Fault like PayloadValidatingInterceptor):

...
public class CustomValidatingInterceptor extends PayloadValidatingInterceptor {

    @Override
    protected boolean handleRequestValidationErrors(MessageContext messageContext, SAXParseException[] errors)
            throws TransformerException {

        // if any validation errors, convert them to a string and throw on as Exception to be handled by CustomSoapErrorMessageDispatcherServlet
        if (errors.length > 0) {
            String validationErrorsString = Arrays.stream(errors)
                    .map(error -> "[" + error.getLineNumber() + "," + error.getColumnNumber() + "]: " + error.getMessage())
                    .collect(Collectors.joining(" -- "));
            throw new CustomSoapValidationException("Validation Errors: " + validationErrorsString);
        }
        return true;
    }
}
...

which is configured in my configuration class (which has to extend from WsConfigurerAdapter now) via

...
public class WebServiceConfig extends WsConfigurerAdapter {
...
    @Override
    public void addInterceptors(List<EndpointInterceptor> interceptors) {
        // validate requests and responses
        // cannot use PayloadValidatingInterceptor because that one would generate an unwanted/unavoidable SoapFault
        CustomValidatingInterceptor validatingInterceptor = new CustomValidatingInterceptor();
        validatingInterceptor.setValidateRequest(true);
        validatingInterceptor.setValidateResponse(false);
        validatingInterceptor.setXsdSchema(customApiSchema());
        interceptors.add(validatingInterceptor);
    }
...

Secondly this now thrown CustomSoapValidationException would still result in a standard SOAP Fault in the endpoint resolving logic, that's why we also create a custom EndpointExceptionResolver. This is called during Exception handling and modifies our interceptor validation error into a "live" exception again which can then bounce up the callstack back into our CustomSoapErrorMessageDispatcherServlet from the first step.

...
// class is automatically picked up by MessageDispatcher during request handling when an exception occurs after dispatching
@Component
public class CustomizedSoapFaultDefinitionExceptionResolver implements EndpointExceptionResolver {
    public boolean resolveException(MessageContext messageContext, Object endpoint, Exception ex) {
        if (ex instanceof CustomSoapValidationException) {
            throw (CustomSoapValidationException) ex;
        }
        return false;
    }
}
...

This does NOT require additional configuration but is automatically picked up by the Spring Boot MessageDispatcher now.

With all those steps all occuring errors/exceptions/failures/... end up one way or another in our CustomSoapErrorMessageDispatcherServlet.doService() where we pick up the exceptions or investigate the not-yet-sent HttpServletResponse and can build a custom SOAP-looking response to meet our requirements.