pythonxmlsoapxsdspyne

How to handle Spyne XSD exceptions gracefully


Whenever my Spyne application receives a request, XSD validation is performed. This is good, but whenever there is an XSD violation a fault is raised and my app returns a Client.SchemaValidationError like so:

<soap11env:Fault>
    <faultcode>soap11env:Client.SchemaValidationError</faultcode>
    <faultstring>:25:0:ERROR:SCHEMASV:SCHEMAV_CVC_DATATYPE_VALID_1_2_1: Element '{http://services.sp.pas.ng.org}DateTimeStamp': '2018-07-25T13:01' is not a valid value of the atomic type 'xs:dateTime'.</faultstring>
    <faultactor></faultactor>
</soap11env:Fault>

I would like to know how to handle the schema validation error gracefully and return the details in the Details field of my service's out_message, rather than just raising a standard Client.SchemaValidationError. I want to store the details of the error as a variable and pass it to my OperationOne function.

Here is my code, I have changed var names for sensitivity.

TNS = "http://services.so.example.org"

class InMessageType(ComplexModel):

    __namespace__ = TNS

    class Attributes(ComplexModel.Attributes):
        declare_order = 'declared'

    field_one = Unicode(values=["ONE", "TWO"],
                      min_occurs=1)
    field_two = Unicode(20, min_occurs=1)
    field_three = Unicode(20, min_occurs=0)
    Confirmation = Unicode(values=["ACCEPTED", "REJECTED"], min_occurs=1)
    FileReason = Unicode(200, min_occurs=0)
    DateTimeStamp = DateTime(min_occurs=1)


class OperationOneResponse(ComplexModel):

    __namespace__ = TNS

    class Attributes(ComplexModel.Attributes):
        declare_order = 'declared'

    ResponseMessage = Unicode(values=["SUCCESS", "FAILURE"], min_occurs=1)
    Details = Unicode(min_len=0, max_len=2000)


class ServiceOne(ServiceBase):

    @rpc(InMessageType,
         _returns=OperationOneResponse,
         _out_message_name='OperationOneResponse',
         _in_message_name='InMessageType',
         _body_style='bare',
         )
    def OperationOne(ctx, message):
        # DO STUFF HERE
        # e.g. return {'ResponseMessage': Failure, 'Details': XSDValidationError}


application = Application([ServiceOne],
                          TNS,
                          in_protocol=Soap11(validator='lxml'),
                          out_protocol=Soap11(),
                          name='ServiceOne',)


wsgi_application = WsgiApplication(application)

if __name__ == '__main__':
    pass

I have considered the following approach but I can't quite seem to make it work yet:

  1. create subclass MyApplication with call_wrapper() function overridden.
  2. Instantiate the application with in_protocol=Soap11(validator=None)
  3. Inside the call wrapper set the protocol to Soap11(validator='lxml') and (somehow) call something which will validate the message. Wrap this in a try/except block and in case of error, catch the error and handle it in whatever way necessary.

I just haven't figured out what I can call inside my overridden call_wrapper() function which will actually perform the validation. I have tried protocol.decompose_incoming_envelope() and other such things but no luck yet.


Solution

  • Overriding the call_wrapper would not work as the validation error is raised before it's called.

    You should instead use the event subsystem. More specifically, you must register an application-level handler for the method_exception_object event.

    Here's an example:

    def _on_exception_object(ctx):
        if isinstance(ctx.out_error, ValidationError):
            ctx.out_error = NicerValidationError(...)
    
    
    app = Application(...)
    app.event_manager.add_listener('method_exception_object', _on_exception_object)
    

    See this test for more info: https://github.com/arskom/spyne/blob/4a74cfdbc7db7552bc89c0e5d5c19ed5d0755bc7/spyne/test/test_service.py#L69


    As per your clarification, if you don't want to reply with a nicer error but a regular response, I'm afraid Spyne is not designed to satisfy that use-case. "Converting" an errored-out request processing state to a regular one would needlessly complicate the already heavy request handling logic.

    What you can do instead is to HACK the heck out of the response document.

    One way to do it is to implement an additional method_exception_document event handler where the <Fault> tag and its contents are either edited to your taste or even swapped out.

    Off the top of my head:

    class ValidationErrorReport(ComplexModel):
        _type_info = [
            ('foo', Unicode), 
            ('bar', Integer32),
        ]
    
    def _on_exception_document(ctx):
        fault_elt, = ctx.out_document.xpath("//soap11:Fault", namespaces={'soap11': NS_SOAP11_ENV})
        explanation_elt = get_object_as_xml(ValidationErrorReport(...))
        fault_parent = fault_elt.parent()
        fault_parent.remove(fault_elt)
        fault_parent.add(explanation_elt)
    

    The above needs to be double-checked with the relevant Spyne and lxml APIs (maybe you can use find() instead of xpath()), but you get the idea.

    Hope that helps!