Savon Handsoap Shootout

This documentation is deprecated, please have a look at savonrb.com!

Looking into The Ruby Toolbox there are currently two popular SOAP client libraries available. In this short article I am going to crunch the candidates Savon, which is currently the most “popular” library, and Handsoap which follows short after. Both are open source projects hosted on github.

Scope

This article won’t cover all facets of both libraries. I concentrate on the features that are relevant for integrating a Ruby SOAP client into our particular SOA platform. That platform is commonly based on Java SOAP services based on frameworks such as CXF and Axis providing interfaces to internal business logic.

Since this is not a complete feature list, it should show you at least how to work with the APIs and which client might be the best choice for yourself.

Requirements

Having lots of Java Guys around here, there is no great focus on things like beautiful API design or Ruby magic, the client should just work! Living in a Java environment, the SOAP client has to integrate smoothly with JRuby. Since a lot of Ruby libraries lack support for JRuby, we always have to monkey patch a lot of code to make it run on the JRE.

Examples

We refer to free, public SOAP services, so everyone can run the examples by themselves.

All the examples seen here can be cloned/downloaded from github.

Chapters

Have fun!

Savon vs. Handsoap: Calling a service

This documentation is deprecated, please have a look at savonrb.com!

The two libraries have different approaches on how to get things done. While Handsoap is using an oldschool inheritance style definition:

class HandsoapBankCode < Handsoap::Service
  
  endpoint :uri => "some_wsdl", :version => 2

  def on_create_document(doc)
    doc.alias "tns", "some_namespace"
  end

  def on_response_document(doc)
    doc.add_namespace "ns1", "some_namespace"
  end
  [...]
end

Savon clients are just a kind of wrapper or proxy around a WSDL:

client = Savon::Client.new "some_wsdl"

While inheritance is a base concept of object oriented programming, it’s usually better to use delegation instead. For not being stuck on the API of the Handsoap::Service class, one would wrap things up into some other class or module, creating more code than necessary.

The proxy style client of Savon is less code and provides a flexible API, especially looking at SOAP calls.

Using rspec to demonstrate the expected behavior of the clients results in two identical spec for getting a zip code of a concrete client implementation:

describe "Savon" do
  it "should return the corrent zip code for a given bank" do
    zip_code = Shootout::SavonBankCode.zip_code @bank_code
    zip_code.should eql @zip_code
  end
end

describe "Handsoap" do
  it "should return the corrent zip code for a given bank" do
    zip_code = Shootout::HandsoapBankCode.zip_code @bank_code
    zip_code.should eql @zip_code
  end
end

Compared to the spec, the code of the two implementations differs a great deal. The task at hand is to call the getBank method of the SOAP endpoint providing a blz (bank code) parameter and extracting the plz (zip code) value of the response.

Using the Handsoap client class defined above, sending the “invoke()” message to the Handsoap::Service will do the job:

def zip_code(bank_code)
  response = invoke("tns:getBank") do |message|
    message.add "tns:blz", bank_code
  end
  (response/"//ns1:details/ns1:plz").first.to_s
end

The bank code parameter is assigned in the block, which yields a SOAP message object. The resulting XML document is wrapped and can be accessed using some predefined XML library. Handsoap enables you to choose between different types of XML parsers like REXML, ruby-libxml or nokogiri.

Savon’s proxy client on the other hand is dynamic and can be accessed directly with the name of the SOAP method and a block:

class SavonBankCode
  def self.zip_code(bank_code)
    client = Savon::Client.new Shootout.endpoints[:bank_code][:uri]
    response = client.get_bank { |soap| soap.body = { "wsdl:blz" => bank_code } }
    response.to_hash[:get_bank_response][:details][:plz]
  end
end

The block yields a SOAP request object for setting the payload or tweaking defaults like the SOAP header. Converting the response to a hash is a convenient way to access the desired result. The conversion is done using crack.

Savon vs. Handsoap: Accessing a WSDL

This documentation is deprecated, please have a look at savonrb.com!

Both clients provide an interface to work with a WSDL. While the Handsoap WSDL support is hidden in some helper class, WSDLs are a first class citizen in Savon. The code for printing out the available SOAP actions looks like this:

require "handsoap/parser"
wsdl = Handsoap::Parser::Wsdl.read(@wsdl_uri)
wsdl.bindings.each {|binding| binding.actions.each{|action| p action.name }}
p Savon::Client.new("some_wsdl").wsdl.soap_actions

The Handsoap parser class is part of a Rails generator. The generator can be used for creating a Handsoap service class skeleton and tests:

$ script/generate handsoap http://www.thomas-bayer.com/axis2/services/BLZService?wsdl

      exists  app
      exists  app/models
      create  app/models/blz_service.rb
      exists  test
      exists  test/integration
      create  test/integration/blz_service_test.rb
----
Endpoints in WSDL
  You should copy these to the appropriate environment files.
  (Eg. `config/environments/*.rb`)
----
# wsdl: http://www.thomas-bayer.com/axis2/services/BLZService?wsdl
BLZ_SERVICE_ENDPOINT = {
  :uri => 'http://www.thomas-bayer.com:80/axis2/services/BLZService',
  :version => 2
}
----

The skeleton provides method stubs for adding request parameters and result parsing:

def get_bank
  soap_action = ''
  response = invoke('tns:getBank', soap_action) do |message|
    raise "TODO"
  end
end

Savon vs. Handsoap: Authentication

This documentation is deprecated, please have a look at savonrb.com!

The libraries provide support for multiple authentication protocols. While Handsoap’s authentication support is more low level, Savon provides an API for that task.

WSSE authentication

As you might expect, the Handsoap way for this aspect is to implement a callback method for the document creation. Using Savon you can attach your credentials on a WSSE object inside your request block. Compare the two implementations:

def on_create_document(doc)
  doc.alias 's', "http://docs...xsd"
  header = doc.find("Header")
  header.add "s:Security" do |s|
    s.set_attr "env:mustUnderstand", "0"
    s.add "s:Username", @@username
    [...]
  end
end
response = client.get_bank do |soap, wsse|
  wsse.username = @@username
  [...]
end

HTTP authentication

The same approach is used by Handsoap to offer HTTP authentication:

def on_after_create_http_request(http_request)
  http_request.set_auth @@username, @@password
end

Savon provides support for this feature just yet (since 0.7.0):

client.request.basic_auth "username", "password"

SSL support

At the time of writing only Savon supports SSL authentication directly. This is achieved by passing a configuration hash to the Savon::Client:

client = Savon::Client.new "some_wsdl"
client.request.http.ssl_client_auth(
  :cert => OpenSSL::X509::Certificate.new(File.read("client_cert.pem")),
  :key => OpenSSL::PKey::RSA.new(File.read("client_key.pem"), "password if one exists"),
  :ca_file => "cacert.pem",
  :verify_mode => OpenSSL::SSL::VERIFY_PEER
)

While the Handsoap documentation states that support for SSL is not yet implemented, there might be a chance to enable it through the use of the preconfigured http driver.

Savon vs. Handsoap: Errors

This documentation is deprecated, please have a look at savonrb.com!

Both client libraries provide the same default behavior for error handling; they raise distinct exceptions for SOAP and HTTP errors:

  • Handsoap::HttpError / Handsoap::Fault
  • Savon::HTTPError / Savon::SOAPFault

And both clients offer a way to override that behavior. Savon lets you surpress errors globally and provides a simple error handling interface on the response object:

Savon::Response.raise_errors = false
[...]
p response.soap_fault
p response.http_error

The same can be achieved in Handsoap by overwriting error hooks in the service implementation:

def on_fault(fault)
  p fault
end

def on_http_error(response)
  p response
end