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 URLaction : String - The SOAP action/method nameenvelope : String - The complete SOAP envelope XMLheaders : Hash (optional) - Additional HTTP headersReturns
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 contentnamespace : String? — SOAP envelope namespace (default: SOAP 1.1)options : Hash? — Wrapping optionsOptions
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: "<script>alert('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 XMLroot_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 elementdata = {
"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"
}
}
}
}
}