soap

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 29, 2026 License: MIT Imports: 16 Imported by: 0

README

SOAP Go

PkgGoDev GoReportCard CI

A Go SDK and CLI tool for SOAP web services.

Features

  • Support for SOAP 1.1, WSDL 1.1, and XSD 1.0
  • Code generation from WSDL files
  • Documentation generation

Developing

go test ./...                # run unit tests
./tools/mage build           # full CI pipeline: generate, lint, test, tidy
./tools/mage integrationtest # integration tests

See AGENTS.md for architecture and design notes.

CLI tool

The soap CLI tool can generate code, documentation, and call SOAP APIs on the fly.

$ soap

  Multi-tool for SOAP APIs

  USAGE

    soap [command] [--flags]  

  CODE GENERATION

    gen [--flags]         Generate code for a SOAP API

  DOCUMENTATION

    doc [--flags]         Display documentation for a SOAP API

  NETWORK OPERATIONS

    call [--flags]        Call a SOAP action

  UTILS

    completion [command]  Generate the autocompletion script for the specified shell
    help [command]        Help about any command

  FLAGS

    -h --help             Help for soap
    -v --version          Version for soap
Installing

The CLI is distributed as a Go module. Install it with the Go toolchain:

go install github.com/tnymlr/soap-go/cmd/soap@latest

The resulting soap binary lands in $(go env GOBIN) (or $(go env GOPATH)/bin if GOBIN is unset).

License

This SDK is published under the MIT License.

Security

Security researchers, please open a private advisory via the Security tab.

Code of Conduct

Be nice.

Documentation

Overview

Package soap provides primitives for SOAP (Simple Object Access Protocol).

Index

Examples

Constants

View Source
const Namespace = "http://schemas.xmlsoap.org/soap/envelope/"

Namespace is the standard SOAP 1.1 envelope namespace

Variables

This section is empty.

Functions

func DefaultCheckRetry

func DefaultCheckRetry(ctx context.Context, err error, request *http.Request, response *http.Response) bool

DefaultCheckRetry implements the default retry logic for HTTP requests.

func UnmarshalHeaderEntry

func UnmarshalHeaderEntry(entry HeaderEntry, dest any) error

UnmarshalHeaderEntry decodes a HeaderEntry back into a typed value — the response-side counterpart of WithHeaderContent. It marshals the entry (preserving the entry's element name, attributes and inner XML) and unmarshals the resulting element into dest.

dest must be a non-nil pointer to a struct whose xml:"..." tag on its XMLName field matches the entry's element name and namespace; otherwise encoding/xml will return an UnmarshalError. Fields on the entry that dest does not declare are silently ignored, matching encoding/xml's usual relaxed behaviour.

Types

type Body

type Body struct {
	XMLName xml.Name

	// Content as raw XML for maximum flexibility
	// This allows both simple payloads and complex nested structures
	Content []byte `xml:",innerxml"`

	// Additional attributes for extensibility
	Attrs []xml.Attr `xml:",any,attr"`
}

Body represents a SOAP body containing the main message payload. As per SOAP 1.1 spec section 4.3, it contains body entries.

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client represents a generic SOAP HTTP client that can handle any type of SOAP request. It works with Envelope types and provides a clean abstraction over HTTP transport.

func NewClient

func NewClient(opts ...ClientOption) (*Client, error)

NewClient creates a new SOAP client with the specified options. Returns an error if the configuration is invalid.

func (*Client) Call

func (c *Client) Call(
	ctx context.Context,
	action string,
	requestEnvelope *Envelope,
	opts ...ClientOption,
) (*Envelope, error)

Call executes a SOAP request with the provided action, envelope, and call-specific options.

type ClientOption

type ClientOption func(*clientConfig)

ClientOption configures a Client using the functional options pattern. Can be used both during client creation and per-call.

func WithCheckRetry

func WithCheckRetry(checkRetry func(context.Context, error, *http.Request, *http.Response) bool) ClientOption

WithCheckRetry sets a custom retry check function. If not provided, uses DefaultCheckRetry for generic HTTP retry logic.

func WithDebug

func WithDebug() ClientOption

WithDebug enables debug output, dumping HTTP requests and responses to stderr. This is a convenience shorthand for:

soap.WithHTTPClient(&http.Client{
    Transport: &soap.DebugTransport{Enabled: &alwaysTrue, Next: http.DefaultTransport},
})

For lazy evaluation (e.g. a --debug flag parsed after client construction), use WithHTTPClient with a DebugTransport directly.

func WithEndpoint

func WithEndpoint(endpoint string) ClientOption

WithEndpoint sets the default SOAP endpoint URL. This can be overridden per call if needed in the future.

func WithHTTPClient

func WithHTTPClient(client *http.Client) ClientOption

WithHTTPClient sets the base HTTP client whose transport is used as the innermost layer of the transport chain (interceptors and retry wrap it). If the client has no Transport, http.DefaultTransport is used. The client's Timeout is ignored — use WithTimeout instead.

func WithInterceptor

func WithInterceptor(interceptor func(http.RoundTripper) http.RoundTripper) ClientOption

WithInterceptor adds a request interceptor for the Client.

func WithMaxRetries

func WithMaxRetries(retries int) ClientOption

WithMaxRetries sets the maximum number of retries for a request. Defaults to 3.

func WithTimeout

func WithTimeout(timeout time.Duration) ClientOption

WithTimeout sets the timeout for the SOAP client.

func WithXMLDeclaration

func WithXMLDeclaration(include bool) ClientOption

WithXMLDeclaration controls whether XML declaration is automatically added to requests. Defaults to true. Set to false if your SOAP service doesn't expect or allow XML declarations.

type DebugTransport

type DebugTransport struct {
	// Enabled controls whether debug output is produced. Checked at
	// request time, so a flag parsed after construction still works.
	Enabled *bool
	// Next is the underlying transport. If nil, [http.DefaultTransport] is used.
	Next http.RoundTripper
}

DebugTransport is an http.RoundTripper that dumps requests and responses to stderr. When Enabled is nil or points to false, requests pass through to Next unchanged.

Standalone CLIs can wire a --debug flag to Enabled:

var debug bool
t := &soap.DebugTransport{Enabled: &debug, Next: http.DefaultTransport}
client, _ := soap.NewClient(soap.WithHTTPClient(&http.Client{Transport: t}))
flag.BoolVar(&debug, "debug", false, "enable debug logging")

func (*DebugTransport) RoundTrip

func (t *DebugTransport) RoundTrip(req *http.Request) (*http.Response, error)

RoundTrip implements http.RoundTripper.

type Detail

type Detail struct {
	// Content as raw XML to accommodate any application-specific fault data
	Content []byte `xml:",innerxml"`

	// Additional attributes for extensibility
	Attrs []xml.Attr `xml:",any,attr"`
}

Detail represents application-specific fault detail information.

type Envelope

type Envelope struct {
	XMLName xml.Name

	// Optional encoding style as per SOAP 1.1 spec section 4.1.1
	EncodingStyle string `xml:"encodingStyle,attr,omitempty"`

	// Optional header as per SOAP 1.1 spec section 4.2
	Header *Header `xml:"Header,omitempty"`

	// Mandatory body as per SOAP 1.1 spec section 4.3
	Body Body `xml:"Body"`

	// Additional attributes for extensibility as per SOAP 1.1 spec section 4.1
	Attrs []xml.Attr `xml:",any,attr"`
}

Envelope represents a SOAP envelope with flexible namespace support. It can handle any namespace prefix and URI, making it compatible with various SOAP implementations. The XMLName field determines the actual element name and namespace used in marshaling/unmarshaling.

Example (Basic)

ExampleEnvelope_basic demonstrates creating a basic SOAP envelope with just a body.

// Create a simple SOAP envelope using the new API
envelope, err := soap.NewEnvelope(soap.WithBody([]byte(`<GetWeather><city>London</city></GetWeather>`)))
if err != nil {
	log.Fatal(err)
}
xmlData, err := xml.MarshalIndent(envelope, "", "  ")
if err != nil {
	log.Fatal(err)
}
fmt.Println(string(xmlData))
Output:
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
  <soapenv:Body><GetWeather><city>London</city></GetWeather></soapenv:Body>
</soapenv:Envelope>
Example (Extensibility)

ExampleEnvelope_extensibility demonstrates using custom attributes for extensibility.

envelope, err := soap.NewEnvelope(soap.WithBody([]byte(`<ProcessOrder><orderId>12345</orderId></ProcessOrder>`)))
if err != nil {
	log.Fatal(err)
}

// Add custom attributes to the body for extensibility
envelope.Body.Attrs = []xml.Attr{
	{Name: xml.Name{Local: "priority"}, Value: "high"},
}

// Add custom attributes to the envelope for extensibility
envelope.Attrs = append(envelope.Attrs, []xml.Attr{
	{Name: xml.Name{Local: "version"}, Value: "1.2"},
	{Name: xml.Name{Local: "trace", Space: "http://example.com/trace"}, Value: "enabled"},
}...)

xmlData, err := xml.MarshalIndent(envelope, "", "  ")
if err != nil {
	log.Fatal(err)
}

fmt.Println(string(xmlData))
Output:
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" version="1.2" xmlns:trace="http://example.com/trace" trace:trace="enabled">
  <soapenv:Body priority="high"><ProcessOrder><orderId>12345</orderId></ProcessOrder></soapenv:Body>
</soapenv:Envelope>
Example (RealWorld)

ExampleEnvelope_realWorld demonstrates creating a SOAP envelope for real-world use.

// Create a SOAP envelope for a weather service request using the new API
envelope, err := soap.NewEnvelope(
	soap.WithBody(
		[]byte(`<GetTemperature xmlns="http://weather.example.com/"><city>Paris</city></GetTemperature>`),
	),
)
if err != nil {
	log.Fatal(err)
}

// Marshal to XML (this is what gets sent to SOAP services)
xmlData, err := xml.MarshalIndent(envelope, "", "  ")
if err != nil {
	log.Fatal(err)
}

fmt.Println(string(xmlData))
Output:
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
  <soapenv:Body><GetTemperature xmlns="http://weather.example.com/"><city>Paris</city></GetTemperature></soapenv:Body>
</soapenv:Envelope>
Example (WithEncodingStyle)

ExampleEnvelope_withEncodingStyle demonstrates setting encoding style.

envelope, err := soap.NewEnvelope(soap.WithBody([]byte(`<GetStockPrice><symbol>AAPL</symbol></GetStockPrice>`)))
if err != nil {
	log.Fatal(err)
}

// Set encoding style for advanced use cases
envelope.EncodingStyle = "http://schemas.xmlsoap.org/soap/encoding/"
xmlData, err := xml.MarshalIndent(envelope, "", "  ")
if err != nil {
	log.Fatal(err)
}
fmt.Println(string(xmlData))
Output:
<soapenv:Envelope encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
  <soapenv:Body><GetStockPrice><symbol>AAPL</symbol></GetStockPrice></soapenv:Body>
</soapenv:Envelope>
Example (WithHeader)

ExampleEnvelope_withHeader demonstrates creating a SOAP envelope with headers.

// Create header entries
mustUnderstand := true
authHeader := soap.HeaderEntry{
	XMLName:        xml.Name{Local: "Authentication", Space: "http://example.com/auth"},
	MustUnderstand: &mustUnderstand,
	Actor:          "http://example.com/gateway",
	Content:        []byte(`<token>abc123xyz</token><user>john.doe</user>`),
}
transactionHeader := soap.HeaderEntry{
	XMLName: xml.Name{Local: "Transaction", Space: "http://example.com/tx"},
	Content: []byte(`<id>tx-456</id>`),
}
// Create envelope with headers using the new API
envelope, err := soap.NewEnvelope(soap.WithBody([]byte(`<GetUserProfile><userId>12345</userId></GetUserProfile>`)))
if err != nil {
	log.Fatal(err)
}

// Add headers manually for advanced use cases
envelope.Header = &soap.Header{
	XMLName: xml.Name{Local: "soapenv:Header"},
	Entries: []soap.HeaderEntry{authHeader, transactionHeader},
}
xmlData, err := xml.MarshalIndent(envelope, "", "  ")
if err != nil {
	log.Fatal(err)
}
fmt.Println(string(xmlData))
Output:
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
  <soapenv:Header>
    <Authentication xmlns="http://example.com/auth" mustUnderstand="true" actor="http://example.com/gateway"><token>abc123xyz</token><user>john.doe</user></Authentication>
    <Transaction xmlns="http://example.com/tx"><id>tx-456</id></Transaction>
  </soapenv:Header>
  <soapenv:Body><GetUserProfile><userId>12345</userId></GetUserProfile></soapenv:Body>
</soapenv:Envelope>

func NewEnvelope

func NewEnvelope(opts ...EnvelopeOption) (*Envelope, error)

NewEnvelope creates a new SOAP envelope with the specified options.

type EnvelopeOption

type EnvelopeOption func(*envelopeConfig)

EnvelopeOption is a function that configures an Envelope.

func WithBody

func WithBody(body any) EnvelopeOption

WithBody sets the body for the Envelope.

func WithHeader

func WithHeader(h *Header) EnvelopeOption

WithHeader sets a pre-constructed SOAP Header on the Envelope. Use this when you need full control over the header (multiple entries, mustUnderstand or actor attributes, etc.). For the common case of a single typed header entry, prefer WithHeaderContent.

func WithHeaderContent

func WithHeaderContent(v any) EnvelopeOption

WithHeaderContent adds a single typed value as a SOAP Header entry. The value is marshalled to XML; its root element's name becomes the HeaderEntry's XMLName and its children become the HeaderEntry's inner content.

Typical usage with generated SOAP types whose XMLName carries the correct namespace:

soap.NewEnvelope(
    soap.WithHeaderContent(&MySoapHeader{Field1: "a", Field2: "b"}),
    soap.WithBody(&MyRequest{}),
)

Multiple WithHeaderContent (and WithHeader) options may be combined to construct a header with several entries; entries are appended in the order the options are applied.

Limitations: attributes on the value's root element (other than xmlns declarations) are not preserved on the HeaderEntry. If you need custom attributes, mustUnderstand, or actor, construct a HeaderEntry directly and pass it via WithHeader.

If marshalling fails, the error is deferred and returned from NewEnvelope. The first such error wins; subsequent header errors are ignored to keep diagnostics focused on the original failure.

func WithNamespace

func WithNamespace(prefix, namespace string) EnvelopeOption

WithNamespace sets the namespace for the Envelope.

type Error

type Error struct {
	// StatusCode is the HTTP status code of the response.
	StatusCode int

	// ResponseBody is the raw HTTP response body.
	ResponseBody []byte

	// Envelope is the SOAP envelope that was received, nil if parsing failed.
	Envelope *Envelope

	// Fault is the SOAP fault, nil if no fault was present.
	Fault *Fault
}

Error contains HTTP and SOAP fault information.

func (*Error) Error

func (e *Error) Error() string

Error implements the error interface.

type Fault

type Fault struct {
	XMLName xml.Name

	// FaultCode is mandatory and provides algorithmic fault identification
	FaultCode string `xml:"faultcode"`

	// FaultString is mandatory and provides human-readable fault description
	FaultString string `xml:"faultstring"`

	// FaultActor is optional and identifies the fault source
	FaultActor string `xml:"faultactor,omitempty"`

	// Detail is optional and contains application-specific error information
	Detail *Detail `xml:"detail,omitempty"`
}

Fault represents a SOAP fault element as per SOAP 1.1 spec section 4.4.

Example

ExampleFault demonstrates creating and handling SOAP faults.

// Create a fault
fault := soap.Fault{
	FaultCode:   "Client",
	FaultString: "Invalid authentication credentials",
	FaultActor:  "http://example.com/auth-service",
	Detail: &soap.Detail{
		Content: []byte(`<error><code>AUTH001</code><message>Token expired</message></error>`),
	},
}
faultXMLData, _ := xml.Marshal(fault)
envelope, err := soap.NewEnvelope(soap.WithBody(faultXMLData))
if err != nil {
	log.Fatal(err)
}
xmlData, err := xml.MarshalIndent(envelope, "", "  ")
if err != nil {
	log.Fatal(err)
}
fmt.Println(string(xmlData))
Output:
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
  <soapenv:Body><Fault><faultcode>Client</faultcode><faultstring>Invalid authentication credentials</faultstring><faultactor>http://example.com/auth-service</faultactor><detail><error><code>AUTH001</code><message>Token expired</message></error></detail></Fault></soapenv:Body>
</soapenv:Envelope>

func (*Fault) String

func (f *Fault) String() string

String returns a comprehensive string representation of the SOAP fault for logging.

type Header struct {
	XMLName xml.Name

	// Header entries - flexible content allowing any XML
	Entries []HeaderEntry `xml:",any"`

	// Additional attributes for extensibility
	Attrs []xml.Attr `xml:",any,attr"`
}

Header represents a SOAP header containing header entries. Each header entry can have mustUnderstand and actor attributes as per SOAP 1.1 spec section 4.2.

type HeaderEntry

type HeaderEntry struct {
	XMLName xml.Name

	// MustUnderstand attribute as per SOAP 1.1 spec section 4.2.3
	// Values: true (1) means mandatory, false (0) or nil means optional
	MustUnderstand *bool `xml:"mustUnderstand,attr,omitempty"`

	// Actor attribute as per SOAP 1.1 spec section 4.2.2
	// Specifies the intended recipient of this header entry
	Actor string `xml:"actor,attr,omitempty"`

	// Content as raw XML for maximum flexibility
	Content []byte `xml:",innerxml"`

	// Additional attributes for extensibility
	Attrs []xml.Attr `xml:",any,attr"`
}

HeaderEntry represents a single header entry with SOAP-specific attributes. Implements the mustUnderstand and actor semantics from SOAP 1.1 spec sections 4.2.2 and 4.2.3.

Example (MustUnderstand)

ExampleHeaderEntry_mustUnderstand demonstrates the mustUnderstand attribute usage.

// Header that MUST be understood by the receiver
mustUnderstand := true
criticalHeader := soap.HeaderEntry{
	XMLName:        xml.Name{Local: "Security", Space: "http://example.com/security"},
	MustUnderstand: &mustUnderstand,
	Content:        []byte(`<signature>digital_signature_here</signature>`),
}

// Header that's optional
optionalHeader := soap.HeaderEntry{
	XMLName: xml.Name{Local: "Metadata", Space: "http://example.com/meta"},
	Content: []byte(`<version>2.0</version>`),
	// MustUnderstand is nil, so it's optional
}

envelope, err := soap.NewEnvelope(
	soap.WithBody([]byte(`<SecureOperation><data>sensitive</data></SecureOperation>`)),
)
if err != nil {
	log.Fatal(err)
}

// Add headers manually for advanced use cases
envelope.Header = &soap.Header{
	XMLName: xml.Name{Local: "soapenv:Header"},
	Entries: []soap.HeaderEntry{criticalHeader, optionalHeader},
}

xmlData, err := xml.MarshalIndent(envelope, "", "  ")
if err != nil {
	log.Fatal(err)
}

fmt.Println(string(xmlData))
Output:
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
  <soapenv:Header>
    <Security xmlns="http://example.com/security" mustUnderstand="true"><signature>digital_signature_here</signature></Security>
    <Metadata xmlns="http://example.com/meta"><version>2.0</version></Metadata>
  </soapenv:Header>
  <soapenv:Body><SecureOperation><data>sensitive</data></SecureOperation></soapenv:Body>
</soapenv:Envelope>

type XSDDateTime

type XSDDateTime struct {
	time.Time
}

XSDDateTime carries an xs:dateTime value while tolerating timezone-less inputs, which the XSD spec permits as "unspecified". Timezone-less inputs are interpreted as UTC. Marshalling emits RFC 3339 with nanosecond precision.

func (XSDDateTime) MarshalText

func (d XSDDateTime) MarshalText() ([]byte, error)

func (*XSDDateTime) UnmarshalText

func (d *XSDDateTime) UnmarshalText(text []byte) error

Directories

Path Synopsis
cmd
soap command
internal
Package wsdl implements the WSDL 1.1 specification.
Package wsdl implements the WSDL 1.1 specification.
Package xsd implements the XSD 1.0 specification.
Package xsd implements the XSD 1.0 specification.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL