Hi all,
I wrote to this list a few weeks ago regarding my problem to get wsdl2ruby to generate proper SOAP faults from operation faults defined in the service wsdl file.
Since I did not receive any replies I decided to take matters into my own hands. I have patched soap4r to handle wsdl-defined faults more gracefully -- allowing the service endpoint to raise wsdl-defined faults which then get properly formatted in a <SOAP:Fault> message. I have cross-tested the solution with Java JAX-WS (
i.e. a java client against a ruby service and vice versa) and it works fine. It would be great if anyone could apply the changes (found below) to the soap4r code base and make them part of the next release.
I guess some testing is necessary to make sure that the changes do not break anything, but from my preliminary testing they appear to work well.
To raise wsdl-defined faults from your service endpoint, the following steps need to be taken:
In your wsdl file:
1) define the type of your fault in a global element declaration:
<element name="AddFault">
<complexType>
<sequence>
<element name="Reason" type="string"/>
<element name="Severity" type="string"/>
</sequence>
</complexType>
</element>
2) define a message that references your fault element:
<message name="AddFaultMessage">
<part name="fault" element="tns:AddFault"/>
</message>
3) Define the fault as a possible fault raised by your operation:
<operation name="Add">
<input name="Add" message="tns:AddMessage"/>
<output name="AddResponse" message="tns:AddResponseMessage"/>
<fault name="AddFault" message="tns:AddFaultMessage"/>
</operation>
4) Add the fault to the operation binding
<binding >
...
<operation name="Add">
<soap:operation soapAction="Add"/>
<input>
<soap:body use="literal"/>
</input>
<output>
<soap:body use="literal"/>
</output>
<fault name="AddFault">
<soap:fault name="AddFault"/>
</fault>
</operation>
...
</binding>
That's it. Now, you should be able to raise the fault from within your service operation code.
raise AddFault.new("Value #{request.value} is too large", "Critical")
and soap4r will reply to the sender with a SOAP fault looking something like:
I have tried to make the wsdl:fault generation adhere to WS-Interoperability (WS-I) conventions:
1) The (single) message part of the fault must refer to a global _element_ declaration (not a type)
2) If the fault binding specifies a "use" attribute, it must have value "literal"
Find the required changes to the soap4r-20061022 code below.
cheers, Peter
diff -rBu ../../soap4r-20061022/lib/soap/rpc/router.rb ./soap/rpc/router.rb
--- ../../soap4r-20061022/lib/soap/rpc/router.rb 2006-10-22 06:29:59.000000000 +0200
+++ ./soap/rpc/router.rb 2007-02-24 11:18:
54.843750000 +0100
@@ -168,8 +168,12 @@
op.call(env.body, @mapping_registry, @literal_mapping_registry,
create_mapping_opt)
default_encodingstyle = op.response_default_encodingstyle
- rescue Exception
- soap_response = fault($!)
+ rescue Exception => e
+ # If a wsdl fault was raised by service, the fault declaration details
+ # is kept in wsdl_fault. Otherwise (exception is a program fault)
+ # wsdl_fault is nil
+ wsdl_fault_details = op.faults[e.class.name]
+ soap_response = fault(e, wsdl_fault_details)
default_encodingstyle = nil
end
conn_data.is_fault = true if soap_response.is_a?(SOAPFault)
@@ -181,7 +185,7 @@
# Create fault response string.
def create_fault_response(e)
- env = SOAPEnvelope.new(SOAPHeader.new, SOAPBody.new
(fault(e)))
+ env = SOAPEnvelope.new(SOAPHeader.new, SOAPBody.new(fault(e, nil)))
opt = {}
opt[:external_content] = nil
response_string = Processor.marshal(env, opt)
@@ -318,20 +322,39 @@
end
# Create fault response.
- def fault(e)
+ def fault(e, wsdl_fault_details)
if e.is_a?(UnhandledMustUnderstandHeaderError)
faultcode = FaultCode::MustUnderstand
else
faultcode = FaultCode::Server
end
- detail = Mapping.obj2soap(Mapping::SOAPException.new(e),
- @mapping_registry)
- detail.elename ||= XSD::QName::EMPTY # for literal mappingregstry
+
+ # If the exception represents a WSDL fault, the fault element should
+ # be added as the SOAP fault <detail> element. If the exception is a
+ # normal program exception, it is wrapped inside a custom SOAP4R
+ # SOAP exception element.
+ detail = nil
+ if (wsdl_fault_details)
+ registry =
+ wsdl_fault_details[:use] == "literal" ? @literal_mapping_registry : @mapping_registry
+ faultQName = XSD::QName.new(wsdl_fault_details[:ns], wsdl_fault_details[:name])
+ detail = Mapping.obj2soap(e, registry, faultQName)
+ # wrap fault element (SOAPFault swallows top-level element)
+ wrapper = SOAP::SOAPElement.new(faultQName)
+ wrapper.add(detail)
+ detail = wrapper
+ else
+ # Exception is a normal program exception. Wrap it.
+ detail = Mapping.obj2soap(Mapping::SOAPException.new(e),
+ @mapping_registry)
+ detail.elename
||= XSD::QName::EMPTY # for literal mappingregstry
+ end
+
SOAPFault.new(
SOAPElement.new(nil, faultcode),
SOAPString.new(e.to_s),
SOAPString.new(@actor),
- detail)
+ detail)
end
def create_mapping_opt
@@ -345,6 +368,7 @@
attr_reader :response_style
attr_reader :request_use
attr_reader :response_use
+ attr_reader :faults
def initialize(soapaction, name, param_def, opt)
@soapaction = soapaction
@@ -353,6 +377,7 @@
@response_style = opt[:response_style]
@request_use = opt[:request_use]
@response_use = opt[:response_use]
+ @faults = opt[:faults]
check_style(@request_style)
check_style(@response_style)
check_use(@request_use)
diff -rBu ../../soap4r-20061022/lib/wsdl/param.rb ./wsdl/param.rb
--- ../../soap4r-20061022/lib/wsdl/param.rb 2005-07-14 14:46:
02.000000000 +0200
+++ ./wsdl/param.rb 2007-02-19 22:54:45.178378900 +0100
@@ -56,7 +56,7 @@
o
when SOAPFaultName
o = WSDL::SOAP::Fault.new
- @soap_fault = o
+ @soapfault = o
o
when DocumentationName
o = Documentation.new
diff -rBu ../../soap4r-20061022/lib/wsdl/soap/classDefCreatorSupport.rb ./wsdl/soap/classDefCreatorSupport.rb
--- ../../soap4r-20061022/lib/wsdl/soap/classDefCreatorSupport.rb 2006-10-01 16:14:
27.000000000 +0200
+++ ./wsdl/soap/classDefCreatorSupport.rb 2007-02-19 16:11:42.000000000 +0100
@@ -54,10 +54,10 @@
#
__EOD__
unless fault.empty?
- faultstr = (fault.collect { |f| dump_inout_type(f).chomp }).join(', ')
+ faultstr = (fault.collect { |f| dump_inout_type(f).chomp }).join("\n")
str <<<<__EOD__
# RAISES
-# #{faultstr}
+#{faultstr}
#
__EOD__
end
diff -rBu ../../soap4r-20061022/lib/wsdl/soap/definitions.rb ./wsdl/soap/definitions.rb
--- ../../soap4r-20061022/lib/wsdl/soap/definitions.rb 2006-10-20 17:51:38.000000000 +0200
+++ ./wsdl/soap/definitions.rb 2007-02-24 12:53:04.484375000 +0100
@@ -82,10 +82,17 @@
collect_fault_messages.each do |name|
faultparts = message(name).parts
if faultparts.size != 1
- raise RuntimeError.new("expecting fault message to have only 1 part")
+ raise RuntimeError.new("Expecting fault message \"#{name}\" to have ONE part")
end
- if result.index(faultparts[0].type).nil?
- result << faultparts[0].type
+ fault_part = faultparts[0]
+ # WS-I Basic Profile Version 1.1 (R2205) requires fault message parts
+ # to refer to elements rather than types
+ if not fault_part.element
+ raise RuntimeError.new("Fault message \"#{name}\" part \"#{fault_part.name}\" must specify an \"element\" attribute")
+ end
+
+ if result.index(fault_part.element).nil?
+ result << fault_part.element
end
end
result
@@ -93,11 +100,54 @@
private
+ def operation_binding(binding, operation_qname)
+ binding.operations.each do |op_binding|
+ if (op_binding.soapoperation_name == operation_qname)
+ return op_binding
+ end
+ end
+ end
+
+ def get_fault_binding(op_binding, fault_name)
+ op_binding.fault.each do |fault|
+ return fault if fault.name == fault_name
+ end
+ return nil
+ end
+
+ def op_binding_declares_fault(op_binding, fault_name)
+ return get_fault_binding(op_binding, fault_name) != nil
+ end
+
def collect_fault_messages
result = []
porttypes.each do |porttype|
porttype.operations.each do |operation|
operation.fault.each do |fault|
+ # Make sure the operation fault has a name
+ if not fault.name
+ raise RuntimeError.new("Operation \"#{
operation.name}\": fault must specify a \"name\" attribute")
+ end
+ operation_qname = XSD::QName.new(operation.targetnamespace, operation.name
)
+ binding = porttype.find_binding()
+ op_binding = operation_binding(binding, operation_qname)
+ # Make sure that portType fault has a corresponding soap:fault
+ # definition in binding section.
+ if not op_binding_declares_fault(op_binding, fault.name)
+ raise RuntimeError.new("Operation \"#{operation.name}\", fault \"#{
fault.name}\": no corresponding wsdl:fault binding found with a matching \"name\" attribute")
+ end
+
+ fault_binding = get_fault_binding(op_binding,
fault.name)
+ if fault_binding.soapfault.name != fault_binding.name
+ puts "WARNING: name of soap:fault \"#{fault_binding.soapfault.name}\" doesn't match the name of wsdl:fault \"#{fault_binding.name}\" in operation \"#{
operation.name}\" \n\n"
+ end
+ # According to WS-I (R2723): if in a wsdl:binding the use attribute
+ # on a contained soapbind:fault element is present, its value MUST
+ # be "literal".
+ if fault_binding.soapfault.use and fault_binding.soapfault.use != "literal"
+ raise RuntimeError.new("Operation \"#{
operation.name}\", fault \"#{fault.name}\": soap:fault \"use\" attribute must be \"literal\"")
+ end
+
if result.index
(fault.message).nil?
result << fault.message
end
diff -rBu ../../soap4r-20061022/lib/wsdl/soap/methodDefCreator.rb ./wsdl/soap/methodDefCreator.rb
--- ../../soap4r-20061022/lib/wsdl/soap/methodDefCreator.rb 2006-10-01 16:14:
27.000000000 +0200
+++ ./wsdl/soap/methodDefCreator.rb 2007-02-24 12:04:09.718750000 +0100
@@ -92,6 +92,19 @@
private
def dump_method(operation, binding)
+ op_faults = {}
+ binding.fault.each
do |fault|
+ op_fault = {}
+ soapfault = fault.soapfault
+ op_fault[:ns] = fault.name.namespace
+ op_fault[:name] = fault.name.name
+ op_fault[:namespace] =
soapfault.namespace
+ op_fault[:use] = soapfault.use || "literal"
+ op_fault[:encodingstyle] = soapfault.encodingstyle || "document"
+ op_faults[
fault.name.name] = op_fault
+ end
+ op_faults_str = op_faults.inspect
+
name = safemethodname(operation.name)
name_as =
operation.name
style = binding.soapoperation_style
@@ -115,7 +128,8 @@
#{dq(name)},
#{paramstr},
{ :request_style => #{sym(style.id2name)}, :request_use => #{sym(inputuse.id2name)},
- :response_style => #{sym(style.id2name)}, :response_use => #{sym(outputuse.id2name)} }
+ :response_style => #{sym(style.id2name)}, :response_use => #{sym(outputuse.id2name)},
+ :faults => #{op_faults_str} }
__EOD__
if inputuse == :encoded or outputuse == :encoded
@encoded = true