ESC
Type to search...
S
Soli Docs

SOAP Class

Make SOAP calls and process XML data for web services.

XML hardening

The XML parser used by SOAP.call and SOAP.parse rejects DOCTYPE declarations outright (XXE / billion-laughs vector), caps element-nesting depth at 64, and caps accumulated text per element at 1 MiB. Legitimate SOAP responses are well below these limits; payloads that hit any cap return a parser error rather than risk runaway memory.

SOAP.call(url, action, envelope, headers?)

SOAP.call(url, action, envelope, headers?)

Makes a SOAP request by performing an HTTP POST with the SOAP envelope. Returns the response Hash directly (auto-resolved).

Parameters

url : String - The SOAP service endpoint URL
action : String - The SOAP action/method name
envelope : String - The complete SOAP envelope XML
headers : Hash (optional) - Additional HTTP headers

Returns

Hash - Response with status, body (raw XML), and parsed (nested Hash)
# Build
body = "<GetWeather xmlns=\"http://example.com/weather\"><City>London</City></GetWeather>"
envelope = SOAP.wrap(body)

# Make the SOAP call (returns Hash directly, no await needed)
result = SOAP.call(
  "https://weather.example.com/service",
  "http://example.com/weather/GetWeather",
  envelope
)

if result["status"] == 200
  response = result["parsed"]["soap:Envelope"]["soap:Body"]["GetWeatherResponse"]
  temp = response["Temperature"]
  condition = response["Condition"]
  
  println("Temperature: " + temp)
  println("Condition: " + condition)
end

SOAP.wrap(body, namespace?, options?)

SOAP.wrap(body, namespace?, options?)

Wraps an XML body in a complete SOAP envelope. Defaults to the SOAP 1.1 namespace and passes the body through verbatim — pass { "escape": true } when the body is untrusted text.

Parameters

body : String — The XML body content
namespace : String? — SOAP envelope namespace (default: SOAP 1.1)
options : Hash? — Wrapping options

Options

escape : Bool — When true, XML-escapes the body before wrapping. Use this whenever body contains user input or other untrusted text. Defaults to false so already-built XML fragments pass through.

Returns

String — Complete SOAP envelope XML
body = "<GetWeather xmlns=\"http://example.com/weather\"><City>London</City></GetWeather>"
envelope = SOAP.wrap(body)
envelope = SOAP.wrap(user_supplied, null, { "escape": true })
# < / & / etc. are encoded so the payload cannot break out of the body

Pick escape: true whenever body contains user input. Without it, an attacker who controls body can inject arbitrary XML into the envelope.

SOAP.parse(xml)

SOAP.parse(xml)

Parses an XML string into a nested Hash structure for easy access.

Parameters

xml : String - XML string to parse

Returns

Hash - Nested Hash with element names as keys
xml = "<?xml version=\"1.0\"?><root><item>value</item></root>"
parsed = SOAP.parse(xml)
# Returns: { "root" => { "item" => { "_text" => "value" } } }

SOAP.xml_escape(text)

SOAP.xml_escape(text)

Escapes special XML characters for safe inclusion in XML documents.

Parameters

text : String - The text to escape

Returns

String - XML-escaped text (<, >, &, ", ')
escaped = SOAP.xml_escape("<script>alert('xss')</script>")
# Returns: "&lt;script&gt;alert(&apos;xss&ript)"

SOAP.to_xml(hash, root_element?)

SOAP.to_xml(hash, root_element?)

Converts a Hash (including nested hashes and arrays) to XML string. Supports attributes with @ prefix and text content with _text key.

Parameters

hash : Hash - The hash to convert to XML
root_element : String (optional) - Root element name (default: "root")

Returns

String - XML string

Special Keys

@name - Creates attribute on parent element
_text - Sets text content of element
data = {
  "user" => {
    "@id" => "123",
    "name" => "John",
    "address" => {
      "street" => "123 Main St",
      "city" => "Boston"
    },
    "tags" => ["admin", "user"]
  }
}

xml = SOAP.to_xml(data, "users")
# Returns XML:
# <users>
#   <user id="123">
#     <name>John</name>
#     <address>
#       <street>123 Main St</street>
#       <city>Boston</city>
#     </address>
#     <tags_0>admin</tags_0>
#     <tags_1>user</tags_1>
#   </user>
# </users>

With _text for Element Content

data = {
  "product" => {
    "name" => "Laptop",
    "description" => {
      "_text" => "A high-performance laptop with 16GB RAM"
    },
    "price" => "999.99"
  }
}

xml = SOAP.to_xml(data, "catalog")
# Returns:
# <catalog>
#   <product>
#     <name>Laptop</name>
#     <description>A high-performance laptop with 16GB RAM</description>
#     <price>999.99</price>
#   </product>
# </catalog>

Complete SOAP Example

# Build the SOAP request body
body = "<GetWeather xmlns=\"http://example.com/weather\"><City>London</City></GetWeather>"
envelope = SOAP.wrap(body)

# Make the SOAP call (auto-resolved, no await needed)
result = SOAP.call(
  "https://weather.example.com/service",
  "http://example.com/weather/GetWeather",
  envelope,
  { "Authorization": "Bearer token123" }
)

# Handle the response
if result["status"] == 200
  response = result["parsed"]["soap:Envelope"]["soap:Body"]["GetWeatherResponse"]
  temp = response["Temperature"]
  condition = response["Condition"]
  
  println("Temperature: " + temp)
  println("Condition: " + condition)
else
  println("Error: " + result["body"])
end

Response Structure

The SOAP.call() returns a Hash with the following structure:

{
 "status": 200,           # HTTP status code
 "status_text": "OK",      # HTTP status text
 "body": "<?xml ...>",  # Raw XML response string
 "headers": {...},         # Response headers Hash
 "parsed": {              # Parsed XML as nested Hash
  "soap:Envelope": {
   "soap:Body": {
    "ResponseElement": {
     "Field1": "value",
     "Field2": "value"
    }
   }
  }
 }
}