Calling a SOAP service in Go

Today the IT world is very focused on high performance, high throughput interfaces. In this situation, it is common to find REST and gRPC API, given their performances compared to the other solutions. Sometimes, though, we still encounter old API written with older techniques or new API that for some reasons have been developed with outdated technologies. One of those cases that I’ve encountered a few times over the last few months is SOAP.

If you are new to the IT world and never encountered the “SOAP” term, it is the abbreviation of “Simple Object Access Protocol”. SOAP is based on the XML format, and often it uses Hypertext Transfer Protocol (HTTP) 1.0 or 1.1 as transmission layer. Not being highly tied to the transmission layer, it does not (uniquely) rely on any transmission layer specific feature such as HTTP Header and HTTP GET and POST parameters.

A typical example of a simple SOAP message is the following:

POST / HTTP/1.1
Host: www.example.org
Content-Type: application/soap+xml; charset=utf-8
Content-Length: 285
SOAPAction: "http://www.w3.org/2003/05/soap-envelope"

<?xml version="1.0"?>
<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope" xmlns:m="http://www.example.org">
  <soap:Header>
  </soap:Header>
  <soap:Body>
    <m:SetHello>
      <m:HelloName>Hello, World!</m:HelloName>
    </m:SetHello>
  </soap:Body>
</soap:Envelope>

This message content weights 285 bytes, which might not seem a lot, but a similar payload in JSON would be 25 bytes and in Protocol Buffers 14 bytes.

Now, when you encounter those API, you might think they are not optimized or smart, but at the end of the day, you need to communicate with them. Many languages like Java and C# comes with a lot of libraries and helpers for SOAP, probably because those languages were heavily used back in the days when SOAP was the default way of implementing API.

In Go, there are some libraries to help with SOAP, but the ones I tried were more in the way then helping, so I decided to go with a slim approach to the problem. Go is natively (with the standard library) able to cast XML to struct and vice-versa. Also, if you abstract the fact that SOAP is a “standard” by itself, it’s just an HTTP request with one additional header (SOAPAction) and a weirdly formatted XML body.

To achieve this, I wrote a couple of functions, that try to help a little without abstracting too much the communication protocol itself.

The first function is soapCall which has the following fingerprint:

soapCall(string, string, interface{}) ([]byte, error)

The idea of this function is that you pass to it the SOAP URL, the SOAP Action and an object with the content of soap:body and the function will prepare the SOAP request, fire it, and return the response as a byte stream and error code. The returned byte stream is useful for then manually processing the XML of the response, but many times it would be easier to unmarshal the response in an object. To achieve this, we can use the second function that has the following fingerprint:

soapCallHandleResponse(string, string, interface{}, result interface{}) error

This case will behave similarly to the previous one, but will also unmarshal the result in an interface pointer object in a similar way to how the XML and JSON unmarshal work.

The code for the soapCallHandleResponse is straightforward, since it will simply call the soapCall function and then the xml.Unmarshal one:

func soapCallHandleResponse(ws string, action string, payloadInterface interface{}, result interface{}) error {
    body, err := soapCall(ws, action, payloadInterface)
    if err != nil {
        return err
    }

    err = xml.Unmarshal(body, &result)
    if err != nil {
        return err
    }

    return nil
}

The soapCall function is slightly more complicated, since it does need to prepare the full payload (which contains all outer boilerplate tags and the real content in soap:Body, then it prepares the HTTP request with all appropriate headers, fires the request and lastly read and return the response.

It’s code is:

func soapCall(ws string, action string, payloadInterface interface{}) ([]byte, error) {
    v := soapRQ{
        XMLNsSoap: "http://schemas.xmlsoap.org/soap/envelope/",
        XMLNsXSD:  "http://www.w3.org/2001/XMLSchema",
        XMLNsXSI:  "http://www.w3.org/2001/XMLSchema-instance",
        Body: soapBody{
            Payload: payloadInterface,
        },
    }
    payload, err := xml.MarshalIndent(v, "", "  ")

    timeout := time.Duration(30 * time.Second)
    client := http.Client{
        Timeout: timeout,
    }

    req, err := http.NewRequest("POST", ws, bytes.NewBuffer(payload))
    if err != nil {
        return nil, err
    }

    req.Header.Set("Accept", "text/xml, multipart/related")
    req.Header.Set("SOAPAction", action)
    req.Header.Set("Content-Type", "text/xml; charset=utf-8")

    dump, err := httputil.DumpRequestOut(req, true)
    if err != nil {
        return nil, err
    }
    fmt.Printf("%q", dump)

    response, err := client.Do(req)
    if err != nil {
        return nil, err
    }

    bodyBytes, err := ioutil.ReadAll(response.Body)
    if err != nil {
        return nil, err
    }

    fmt.Println(string(bodyBytes))
    defer response.Body.Close()
    return bodyBytes, nil
}

The whole code ends up being:

package soap

import (
        "bytes"
        "encoding/xml"
        "fmt"
        "io/ioutil"
        "net/http"
        "net/http/httputil"
        "time"
)

type soapRQ struct {
        XMLName   xml.Name `xml:"soap:Envelope"`
        XMLNsSoap string   `xml:"xmlns:soap,attr"`
        XMLNsXSI  string   `xml:"xmlns:xsi,attr"`
        XMLNsXSD  string   `xml:"xmlns:xsd,attr"`
        Body      soapBody
}

type soapBody struct {
        XMLName xml.Name `xml:"soap:Body"`
        Payload interface{}
}

func soapCallHandleResponse(ws string, action string, payloadInterface interface{}, result interface{}) error {
        body, err := soapCall(ws, action, payloadInterface)
        if err != nil {
                return err
        }

        err = xml.Unmarshal(body, &result)
        if err != nil {
                return err
        }

        return nil
}

func soapCall(ws string, action string, payloadInterface interface{}) ([]byte, error) {
        v := soapRQ{
                XMLNsSoap: "http://schemas.xmlsoap.org/soap/envelope/",
                XMLNsXSD:  "http://www.w3.org/2001/XMLSchema",
                XMLNsXSI:  "http://www.w3.org/2001/XMLSchema-instance",
                Body: soapBody{
                        Payload: payloadInterface,
                },
        }
        payload, err := xml.MarshalIndent(v, "", "  ")

        timeout := time.Duration(30 * time.Second)
        client := http.Client{
                Timeout: timeout,
        }

        req, err := http.NewRequest("POST", ws, bytes.NewBuffer(payload))
        if err != nil {
                return nil, err
        }

        req.Header.Set("Accept", "text/xml, multipart/related")
        req.Header.Set("SOAPAction", action)
        req.Header.Set("Content-Type", "text/xml; charset=utf-8")

        dump, err := httputil.DumpRequestOut(req, true)
        if err != nil {
                return nil, err
        }
        fmt.Printf("%q", dump)

        response, err := client.Do(req)
        if err != nil {
                return nil, err
        }

        bodyBytes, err := ioutil.ReadAll(response.Body)
        if err != nil {
                return nil, err
        }

        fmt.Println(string(bodyBytes))
        defer response.Body.Close()
        return bodyBytes, nil
}

I’ve enjoyed this approach, mainly because this allowed me to speed up the development and to improve the quality of the code without generating a full abstraction layer over the communication itself, which would have diminished the flexibility I had on the connection.