1

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.

teebagz
  • 656
  • 1
  • 4
  • 26

1 Answers1

3

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!

Burak Arslan
  • 7,671
  • 2
  • 15
  • 24
  • Hello, thank you so much for taking the time to answer this question. I have tried both approaches. I'm not sure why but the _on_method_exception event appears not to be fired in case of an error. I have added listener for each possible event of application and wsgiapplication in case of a valid request i get >_on_context_created >_on_wsgi_call >_on_call >_on_return_object >_on_wsgi_return >_on_context_closed >_on_wsgi_close In case of invalid request i get: >_on_context_created >_on_wsgi_call >_on_context_closed >_on_wsgi_close – teebagz Aug 07 '19 at 14:59
  • So where there is SchemaValidationError, it seems the overridden call_wrapper function is never called, and the method_exception event is never fired. I will take a further look at service and protocol events to see what i can find. – teebagz Aug 07 '19 at 15:03
  • I have now added added a listener to every possible event across all four classes which support firing events, it seems that the only four events which get fired on case of xsd validation error are _on_context_created, _on_wgi_call, _on_context_closed and _on_wsgi_close - this is when i have validator set to 'lxml' – teebagz Aug 07 '19 at 15:48
  • 1
    That is a bug. Please try again with spyne-2.13.11-alpha – Burak Arslan Aug 08 '19 at 00:00
  • Indeed all the correct events appear to be firing with this version thank you. I think maybe i wasn't clear about what i am trying to do. I don't just want to give a nicer validation error inside soap fault element. I want to avoid sending soap fault element and instead send details of failure inside the ResponseType that i have defined. I think (possibly i am wrong because i am far from expert) this is the part that is contravention of SOAP standard, because faults should be given as faults, but i am working to a spec made by someone else – teebagz Aug 08 '19 at 14:02
  • 1
    "Converting" an errored-out request processing state to a regular one is not possible. I wouldn't accept a patch that adds that kind of functionality to Spyne. See my updated answer. – Burak Arslan Aug 08 '19 at 15:09
  • Thank you - If i understand correctly then the comments between line 92 and line 98 explain exactly why this is not possible. As i mentioned i am working to someone else's spec and I have already been back to them to explain that i have reservations about giving error responses in their way. I now know exactly how to explain why it is wrong to do it this way. Thank you for helping me, btw i think Spyne is amazing you do great work! – teebagz Aug 08 '19 at 15:21
  • Would this be the same story for a validation caused by a unicode field with choices such as `choicefield = unicode(choices=['a', 'b'])` but the request contains `c`? – teebagz Aug 08 '19 at 15:35