Savon Guide
Savon is a SOAP client library for Ruby. It’s goal is to provide a lightweight and easy to use alternative to soap4r. If you’re starting to use Savon, please make sure to read this guide and make yourself familiar with SOAP itself, WSDL documents and tools like soapUI.
Table of contents
- Installation
- Runtime dependencies
- Getting started
- The WSDL object
- The HTTP object
- The WSSE object
- Executing SOAP requests
- The SOAP object
- The Response object
- Error handling
- Global configuration
- Ecosystem
- Alternative libraries
Installation
Savon is available through Rubygems and can be installed via:
$ gem install savon
Runtime dependencies
HTTPI is an interface supporting multiple HTTP libraries. It’s a crucial part of Savon and you should make sure to get familiar with it.
Getting started
Savon is based around the Savon::Client object. It represents a particular SOAP service and let’s you configure and execute SOAP requests. Let’s create a client using a remote WSDL document:
client = Savon::Client.new do
wsdl.document = "http://service.example.com?wsdl"
end
Savon::Client.new
accepts a block to be evaluated in the context of the client object. Inside this block, you can access all methods from your own class, but local variables won’t work. For more information on this, I recommend you read about instance_eval with delegation.
If you don’t like this or if it’s creating a problem for you, you can use block arguments to specify which objects you would like to receive and Savon will yield those instead of instance evaluating the block. The .new
method accepts 1-3 arguments and yields the following objects:
[wsdl, http, wsse]
For example, to work with the wsdl and http object, you can specify only two of the three possible arguments:
client = Savon::Client.new do |wsdl, http|
wsdl.document = "http://service.example.com?wsdl"
http.proxy = "http://proxy.example.com"
end
The three objects mentioned above can also be used after instantiating the client (outside of the block). For example:
client.wsse.credentials "username", "password"
The next sections should give you a pretty good impression on how these objects can be used.
The WSDL object
The wsdl object is actually called Savon::WSDL::Document, but I’ll refer to these objects by shortnames. The wsdl object is a representation of a WSDL document.
Inspecting a Service
Specifying the location of a WSDL document gives you access to a couple of methods for inspecting your service.
# specifies a remote location
wsdl.document = "http://service.example.com?wsdl"
# uses a local document
wsdl.document = "../wsdl/authentication.xml"
The following examples assume you specified a WSDL location.
# returns the target namespace
wsdl.namespace # => "http://v1.example.com"
# returns the SOAP endpoint
wsdl.endpoint # => "http://service.example.com"
# returns an Array of available SOAP actions
wsdl.soap_actions # => [:create_user, :get_user, :get_all_users]
# returns the WSDL document as a String
wsdl.to_xml # => "<wsdl:definitions name=\"AuthenticationService\" ..."
Note: your service probably uses (lower)CamelCase method and object names, but Savon maps those to snake_case Symbols for you.
Working without a WSDL
Retrieving and parsing WSDL documents is a quite expensive operation. And even though Savon caches the result, my recommendation is to not use a WSDL document (at least in production) and directly access the SOAP endpoint instead. This requires you to specify the SOAP endpoint and target namespace instead of a WSDL location:
client = Savon::Client.new do
wsdl.endpoint = "http://service.example.com"
wsdl.namespace = "http://v1.example.com"
end
The HTTP object
HTTPI::Request is provided by the HTTPI gem and represents an HTTP request. Savon executes a GET request to retrieve remote WSDL documents and POST requests for each SOAP request.
I’m only going to document a few interesting details and point you to the HTTPI documentation for additional information.
Note: HTTPI is still a very young project and might not support everything you need. Please don’t hesitate to file bugs or make wishes for the library to support additional features.
SSL and HTTP headers
You can easily set the SSL certificate and custom HTTP headers for your requests:
client = Savon::Client.new do
http.auth.ssl.cert_key_file = 'cert.key'
http.auth.ssl.cert_key_password = 'C3rtP@ssw0rd'
http.auth.ssl.cert_file = 'cert.crt'
http.auth.ssl.verify_mode = :none
http.read_timeout = 90
http.open_timeout = 90
http.headers = { "Accept-Encoding" => "gzip, deflate", "Connection" => "Keep-Alive" }
end
Headers can also be changed on subsequent calls of the client.request using the same approach.
SOAPAction
SOAPAction is an HTTP header information required by legacy services. If present, the header value must have double quotes surrounding the URI-reference (SOAP 1.1. spec, section 6.1.1). Here’s how you would set/overwrite the SOAPAction header:
http.headers["SOAPAction"] = '"urn:example#service"'
Cookies
If your service relies on cookies to handle sessions, you can grab the cookie from the HTTPI::Response and set it for the next request:
client.http.headers["Cookie"] = response.http.headers["Set-Cookie"]
The WSSE object
Savon::WSSE allows you to use WSSE authentication (PDF).
# sets the WSSE credentials
wsse.credentials "username", "password"
# enables WSSE digest authentication
wsse.credentials "username", "password", :digest
Executing SOAP requests
Now for the fun part. To execute SOAP requests, Savon::Client#request
is the way to go. Let’s look at a very basic example of executing a SOAP request to a get_all_users
action.
response = client.request :get_all_users
This single argument (the name of the SOAP action to call) works in different ways depending on whether you specified a WSDL document to use. If you did, Savon will parse the WSDL document for available SOAP actions and convert their names to snake_case Symbols for you. When you’re not using a WSDL, the argument will (by convention) be converted to lowerCamelCase.
:get_all_users.to_s.lower_camelcase # => "getAllUsers"
:get_pdf.to_s.lower_camelcase # => "getPdf"
This convention might not work for you if your service requires CamelCase method names or methods with UPPERCASE acronyms. But don’t worry. If you pass in a String instead of a Symbol, Savon will not convert the argument.
response = client.request "GetPDF"
The argument(s) passed to the #request
method will affect the SOAP input tag inside the SOAP request. To make sure you know what this means, here’s an example for a simple request:
<env:Envelope
xmlns:env="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<env:Body>
<getAllUsers /> <!-- the SOAP input tag -->
</env:Body>
</env:Envelope>
By now you should know the result of passing a single argument. But fairly often you need to prefix the input tag with the target namespace of your service like this:
<env:Envelope
xmlns:env="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:wsdl="http://v1.example.com">
<env:Body>
<wsdl:getAllUsers />
</env:Body>
</env:Envelope>
If you pass two arguments to the #request
method, the first (a Symbol) will be used for the namespace and the second (a Symbol or String) will be the SOAP action to call:
response = client.request :wsdl, :get_all_users
On rare occasions, you may actually need to attach XML attributes to the input tag. In that case, you can pass a Hash of attributes to the name of your SOAP action and the optional namespace:
response = client.request :wsdl, "GetPDF", :id => 1
These three arguments will generate the following input tag:
<wsdl:GetPDF id="1" />
Since most SOAP actions require you to pass arguments for e.g. the user to return, you need to send a “payload”. Luckily you’re already familiar with passing a block to a method, right? Savon::Client#request
also accepts a block for you to access the following objects:
[soap, wsdl, http, wsse]
Notice, that the list is almost the same as the one for Savon::Client.new
. Except now, there is an additional object called soap. In contrast to the other three objects, the soap object is tied to single requests. Savon creates a new soap object for every request.
The SOAP object
Savon::SOAP::XML is tied to a single SOAP request and lets you customize the SOAP request XML.
SOAP version
Savon by default expects your services to be based on SOAP 1.1. For SOAP 1.2 services, you can set the SOAP version per request:
response = client.request :get_user do
soap.version = 2
end
Namespaces
If you don’t pass a namespace to Savon::Client#request
, Savon will register the target namespace (“xmlns:wsdl”) for you. If you did pass a namespace, Savon will use it instead of the default one. For example:
client.request :v1, :get_user
<env:Envelope
xmlns:env="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:v1="http://v1.example.com">
<env:Body>
<v1:GetUser>
</env:Body>
</env:Envelope>
You can always set namespaces or overwrite namespaces set by Savon. Namespaces are stored as a simple Hash.
# setting a new namespace
soap.namespaces["xmlns:g2"] = "http://g2.example.com"
# overwriting the "xmlns:wsdl" namespace
soap.namespaces["xmlns:wsdl"] = "http://ns.example.com"
SOAP body
You probably need to specify some arguments required by the SOAP action you’re going to call. If you’re, for example, interacting with a get_user
action which expects the ID of the user to return, you can simply pass a Hash:
response = client.request :get_user do
soap.body = { :id => 1 }
end
As you already saw before, Savon is based on a few conventions to make the experience of having to work with SOAP and XML as pleasant as possible. The Hash passed to Savon::SOAP::XML#body=
is not an exception. It is translated to XML using the Hash#to_soap_xml
method provided by Savon.
Here’s a more complex example:
response = client.request :wsdl, "CreateUser" do
soap.body = {
:first_name => "The",
:last_name => "Hoff",
"FAME" => ["Knight Rider", "Baywatch"]
}
end
As with the SOAP action, Symbol keys will be converted to lowerCamelCase and String keys won’t be touched. The previous example generates the following XML:
<env:Envelope
xmlns:env="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:wsdl="http://v1.example.com">
<env:Body>
<wsdl:GetUser>
<firstName>The</firstName>
<lastName>Hoff</lastName>
<FAME>Knight Rider</FAME>
<FAME>Baywatch</FAME>
</wsdl:GetUser>
</env:Body>
</env:Envelope>
Some services actually require the XML elements to be in a specific order. If you don’t use Ruby 1.9 (and you should), you can not be sure about the order of Hash elements and have to specify the correct order using an Array under a special :order!
key:
{ :last_name => "Hoff", :first_name => "The", :order! => [:first_name, :last_name] }
This will make sure, that the lastName tag follows the firstName.
Assigning arguments to XML tags using a Hash is even more difficult. It requires another Hash under an attributes!
key containing a key matching the XML tag and the Hash of attributes to add:
{ :first_name => "TheHoff", :last_name => nil, :attributes! => { :last_name => { "xsi:nil" => true } } }
This example will be translated to the following XML:
<firstName>TheHoff</firstName><lastName xsi:nil="true"></lastName>
I would not recommend using a Hash for the SOAP body if you need to create complex XML structures, because there are better alternatives. One of them is to pass a block to the Savon::SOAP::XML#body
method. Savon will then yield a Builder::XmlMarkup
instance for you to use.
soap.body do |xml|
xml.firstName("The")
xml.lastName("Hoff")
end
Last but not least, you can also create and use a simple String (created with Builder or any another tool):
soap.body = "<firstName>The</firstName><lastName>Hoff</lastName>"
SOAP header
Besides the body element, SOAP requests can also contain a header with additional information. Savon sees this header as just another Hash following the same conventions as the SOAP body Hash.
soap.header = { "SecretKey" => "secret" }
Custom XML
If you’re sure that none of these options work for you, you can completely customize the XML to be used for the SOAP request:
soap.xml = "<custom><soap>request</soap></custom>"
The Response object
Savon::Client#request
returns a Savon::SOAP::Response for you to work with. While Savon::SOAP::Response#to_hash
converts the SOAP response XML to a Ruby Hash:
response.to_hash # => { :response => { :success => true, :name => "John" } }
Savon::SOAP::Response#to_xml
simply returns the original SOAP response XML:
response.to_xml # => "<response><success>true</success><name>John</name></response>"
The response also contains the HTTPI::Response:
response.http # => #<HTTPI::Response:0x1017b4268 ...
Error handling
By default, Savon raises both Savon::SOAP::Fault
and Savon::HTTP::Error
when encountering these kind of errors.
begin
client.request :get_all_users
rescue Savon::SOAP::Fault => fault
log fault.to_s
end
Both errors inherit from Savon::Error
, so you don’t need to explicitly rescue both:
begin
client.request :get_all_users
rescue Savon::Error => error
log error.to_s
end
If you changed the default to not raise these errors, you can ask the response whether the request was successful:
response.success? # => false
response.soap_fault? # => true
response.http_error? # => false
You can then access the error objects mentioned above:
response.soap_fault # => Savon::SOAP::Fault
response.http_error # => Savon::HTTP::Error
Please notice, that these methods always return an error object. To check if an error is actually present, you can either ask the response or directly ask the error object:
response.soap_fault.present? # => true
response.http_error.present? # => false
Global configuration
Logging
By default, Savon logs each SOAP request and response to STDOUT using a log level of :debug.
Savon.configure do |config|
config.log = false # disable logging
config.log_level = :info # changing the log level
config.logger = Rails.logger # using the Rails logger
end
Error handling
If you don’t like to rescue errors, here’s how you can tell Savon to not raise them:
Savon.configure do |config|
config.raise_errors = false # do not raise SOAP faults and HTTP errors
end
SOAP version
Also changing the default SOAP version of 1.1 to 1.2 is fairly easy:
Savon.configure do |config|
config.soap_version = 2 # use SOAP 1.2
end
Ecosystem
Savon::Model
Savon::Model creates SOAP service oriented models.
Savon::Spec
Savon::Spec helps you test your SOAP requests.
Alternative libraries
And if you feel like there’s no way Savon will fit your needs, you should take a look at The Ruby Toolbox to find some alternatives.