Using SOAP::Lite With Perl

I've been trying to access a SOAP service using Perl for a project, so I took a quick look at CPAN to see what was available. The answer seemed to be to use SOAP::Lite.

This module turned out to be a real devil to use, and my problems with it were probably compounded by my lack of SOAP experience.

SOAP (incase you didn't know) is a method of accessing remote services using XML. In my case, I was trying to access content supplied by a third party ringtone provider using SOAP over HTTP.

Using SOAP::Lite you have to specify a uri and a proxy. What stumped me for a while is that the proxy is the URI of service you wish to access, and uri is the namespace of the service.

So if the provider I was using had their SOAP service available at ws.robstones-services.co.uk/external.asmx, I would use this as the proxy, and in this case I would use ws.robstones-services.co.uk/External as the urinamespace.

Great, I knew the service I needed was called getCallList, and required a Username and Password to be passed to it. In return it would give me a list of valid content types. The SOAPAction header to be added to the call to proxy, letting the remote SOAP service I wanted to use the service http://ws.robstones-services.co.uk/External/getCallList.

My first attempt was the following code...

#!/usr/bin/perl -w use strict; use SOAP::Lite 'trace', 'debug'; my $server = SOAP::Lite ->uri('http://ws.robstones-services.co.uk/External') ->proxy('http://ws.robstones-services.co.uk/external.asmx'); my $returned = $server ->getCallList({ 'Username' => 'RobsUser', 'Password' => 'RobsPassword' }); foreach my $type ($returned->valueof('//getCallListResult/string')) { next unless ($type); ## ignore any undefs print "$type\n"; }

You'll notice I'm using trace and debug to see the SOAP messages being sent and received. The dialogue from this script was...

> perl -w test.pl SOAP::Transport::HTTP::Client::send_receive: POST http://ws.robstones-services.co.uk/external.asmx HTTP/1.1 Accept: text/xml Accept: multipart/* Content-Length: 615 Content-Type: text/xml; charset=utf-8 SOAPAction: "http://ws.robstones-services.co.uk/External#getCallList" <?xml version="1.0" encoding="UTF-8"?><SOAP-ENV:Envelope xmlns:xsi="http://www.w3.org/1999/XMLSchema-instance" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsd="http://www.w3.org/1999/XMLSchema" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"><SOAP-ENV:Body><namesp1:getCallList xmlns:namesp1="http://ws.robstones-services.co.uk/External"><c-gensym3><Username xsi:type="xsd:string">RobsUser</Username><Password xsi:type="xsd:string">RobsPassword</Password></c-gensym3></namesp1:getCallList></SOAP-ENV:Body></SOAP-ENV:Envelope> SOAP::Transport::HTTP::Client::send_receive: HTTP/1.1 500 (Internal Server Error) Internal Server Error. Cache-Control: private Connection: close Date: Tue, 29 Nov 2005 15:23:38 GMT Server: Microsoft-IIS/6.0 Content-Length: 508 Content-Type: text/xml; charset=utf-8 Client-Date: Tue, 29 Nov 2005 15:23:09 GMT Client-Response-Num: 1 X-AspNet-Version: 1.1.4322 X-Powered-By: ASP.NET <?xml version="1.0" encoding="utf-8"?> <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <soap:Body> <soap:Fault> <faultcode>soap:Client</faultcode> <faultstring>Server did not recognize the value of HTTP Header SOAPAction: http://ws.robstones-services.co.uk/External#getCallList.</faultstring> <detail /> </soap:Fault> </soap:Body> </soap:Envelope>

The call didn't work, you'll see the SOAPAction variable is wrong, it's http://ws.robstones-services.co.uk/External#getCallList instead of http://ws.robstones-services.co.uk/External/getCallList.

I needed to get SOAP::Lite to use the correct URL. The secret turned out to be to change the use SOAP::Lite line to the following.

use SOAP::Lite on_action =*gt; sub {sprintf '%s/%s', @_}, 'trace', 'debug';

on_action is a parameter SOAP::Lite uses to separate the URI from the action, here we're telling it to use /.

This gave me the following SOAP dialogue...

> perl -w test.pl SOAP::Transport::HTTP::Client::send_receive: POST http://ws.robstones-services.co.uk/external.asmx HTTP/1.1 Accept: text/xml Accept: multipart/* Content-Length: 615 Content-Type: text/xml; charset=utf-8 SOAPAction: http://ws.robstones-services.co.uk/External/getCallList <?xml version="1.0" encoding="UTF-8"?><SOAP-ENV:Envelope xmlns:xsi="http://www.w3.org/1999/XMLSchema-instance" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsd="http://www.w3.org/1999/XMLSchema" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"><SOAP-ENV:Body><namesp1:getCallList xmlns:namesp1="http://ws.robstones-services.co.uk/External"><c-gensym3><Username xsi:type="xsd:string">RobsUser</Username><Password xsi:type="xsd:string">RobsPassword</Password></c-gensym3></namesp1:getCallList></SOAP-ENV:Body></SOAP-ENV:Envelope> SOAP::Transport::HTTP::Client::send_receive: HTTP/1.1 200 OK Cache-Control: private, max-age=0 Connection: close Date: Tue, 29 Nov 2005 15:22:53 GMT Server: Microsoft-IIS/6.0 Content-Length: 405 Content-Type: text/xml; charset=utf-8 Client-Date: Tue, 29 Nov 2005 15:22:25 GMT Client-Response-Num: 1 X-AspNet-Version: 1.1.4322 X-Powered-By: ASP.NET <?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body><getCallListResponse xmlns="http://ws.robstones-services.co.uk/External"><getCallListResult><string xsi:nil="true" /></getCallListResult></getCallListResponse></soap:Body></soap:Envelope>

Bugger, it still didn't work. I looked again at the SOAP message being sent. It was sending a lot of namespace information and a wrapper around the Username and Password. The wrapper was definitely wrong, and the remote .NET service didn't seem to like the namespaces.

I needed to sort that out, so I had to tell SOAP::Lite to not to include namespaces or wrap up the Username and Password. The way to do this is to use SOAP::Data to hardcode the data being sent.

This meant changing the call to getCallList to the following...

my $returned = $server ->getCallList( SOAP::Data->name('Username')->value('RobsUser')->type(''), SOAP::Data->name('Password')->value('RobsPassword')->type('') );

The SOAP dialogue after these changes looked like this..

SOAP::Transport::HTTP::Client::send_receive: POST http://ws.robstones-services.co.uk/external.asmx HTTP/1.1 Accept: text/xml Accept: multipart/* Content-Length: 548 Content-Type: text/xml; charset=utf-8 SOAPAction: http://ws.robstones-services.co.uk/External/getCallList <?xml version="1.0" encoding="UTF-8"?><SOAP-ENV:Envelope xmlns:xsi="http://www.w3.org/1999/XMLSchema-instance" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsd="http://www.w3.org/1999/XMLSchema" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"><SOAP-ENV:Body><namesp1:getCallList xmlns:namesp1="http://ws.robstones-services.co.uk/External"><Username>RobsUser</Username><Password>RobsPassword</Password></namesp1:getCallList></SOAP-ENV:Body></SOAP-ENV:Envelope> SOAP::Transport::HTTP::Client::send_receive: HTTP/1.1 200 OK Cache-Control: private, max-age=0 Connection: close Date: Tue, 29 Nov 2005 15:19:49 GMT Server: Microsoft-IIS/6.0 Content-Length: 405 Content-Type: text/xml; charset=utf-8 Client-Date: Tue, 29 Nov 2005 15:19:21 GMT Client-Response-Num: 1 X-AspNet-Version: 1.1.4322 X-Powered-By: ASP.NET <?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body><getCallListResponse xmlns="http://ws.robstones-services.co.uk/External"><getCallListResult><string xsi:nil="true" /></getCallListResult></getCallListResponse></soap:Body></soap:Envelope>

Close, but still not right. The problem was with the getCallList itself, it was still trying to use a namespace.

The final solution was to this problem was to not use getCallList, but instead to use SOAP::Lite's call method to implicitly set how the function was being called.

The code after making this change was the following...

my $returned = $server ->call(SOAP::Data->name('getCallList')->attr({xmlns => 'http://ws.robstones-services.co.uk/External'}) => SOAP::Data->name('Username')->value('RobsUser')->type(''), SOAP::Data->name('Password')->value('RobsPassword')->type('') );

This gave me the SOAP dialogue...

> perl -w test_soap.pl SOAP::Transport::HTTP::Client::send_receive: POST http://ws.robstones-services.co.uk/external.asmx HTTP/1.1 Accept: text/xml Accept: multipart/* Content-Length: 524 Content-Type: text/xml; charset=utf-8 SOAPAction: http://ws.robstones-services.co.uk/External/getCallList <?xml version="1.0" encoding="UTF-8"?><SOAP-ENV:Envelope xmlns:xsi="http://www.w3.org/1999/XMLSchema-instance" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsd="http://www.w3.org/1999/XMLSchema" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"><SOAP-ENV:Body><getCallList xmlns="http://ws.robstones-services.co.uk/External"><Username>RobsUser</Username><Password>RobsPassword</Password></getCallList></SOAP-ENV:Body></SOAP-ENV:Envelope> SOAP::Transport::HTTP::Client::send_receive: HTTP/1.1 200 OK Cache-Control: private, max-age=0 Connection: close Date: Tue, 29 Nov 2005 15:16:07 GMT Server: Microsoft-IIS/6.0 Content-Length: 540 Content-Type: text/xml; charset=utf-8 Client-Date: Tue, 29 Nov 2005 15:15:39 GMT Client-Response-Num: 1 X-AspNet-Version: 1.1.4322 X-Powered-By: ASP.NET <?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body><getCallListResponse xmlns="http://ws.robstones-services.co.uk/External"><getCallListResult><string>ROBS_JAVA</string><string>ROBS_WALL</string><string>ROBS_POLY</string><string>ROBS_MONO</string><string>ROBS_REAL</string><string xsi:nil="true" /></getCallListResult></getCallListResponse></soap:Body></soap:Envelope> ROBS_JAVA ROBS_WALL ROBS_POLY ROBS_MONO ROBS_REAL

Hurrah, after all that effort, I have a list of 5 types of content offered by my third party supplier. I can now go ahead and build my service.

SOAP::Lite is a powerful module, but the lack of simple, easy to follow documentation and examples holds it back. I hope this small article helps out other programmers just starting on the SOAP path.

Entered: 2005-11-29 21:20:58

Rob's Other Blog Entries

See other blog entries for November 2005, or an index of all blog entries.