Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Quick Start

A complete, runnable service is in the repository as the simple_service example with its WSDL fixture hello.wsdl. Run it with cargo run --example simple_service. This page walks that example from WSDL operation to wire response.

The contract: one operation in the WSDL

hello.wsdl declares a single document/literal operation. The pieces that matter for wiring a handler are the operation name and its input/output element names:

<xs:element name="SayHello">                 <!-- request body element -->
  <xs:complexType><xs:sequence>
    <xs:element name="Name" type="xs:string" minOccurs="1"/>
  </xs:sequence></xs:complexType>
</xs:element>
<xs:element name="SayHelloResponse">         <!-- response body element -->
  <xs:complexType><xs:sequence>
    <xs:element name="Greeting" type="xs:string" minOccurs="1"/>
  </xs:sequence></xs:complexType>
</xs:element>
...
<wsdl:operation name="SayHello"> ... </wsdl:operation>

The server

use soap_server::{escape_text, FnHandler, ServerBuilder, SoapFault};
use bytes::Bytes;

#[tokio::main]
async fn main() {
    let svc = ServerBuilder::from_wsdl_file("examples/hello.wsdl")
        // "SayHello" MUST match the <wsdl:operation name="..."> above, or
        // .build() returns Err — misnamed handlers fail at startup.
        .handler(
            "SayHello",
            FnHandler::new(|body: Bytes| async move {
                // `body` is the <SayHello> element. Parse out <Name> (see the
                // example for the quick-xml read_text + unescape helper)...
                let name = parse_name(&body).unwrap_or_else(|| "world".into());
                // ...then return the <SayHelloResponse> element. The library wraps
                // it in the SOAP envelope verbatim, so escape any text you inject.
                let xml = format!(
                    r#"<SayHelloResponse xmlns="urn:example:hello"><Greeting>Hello, {}!</Greeting></SayHelloResponse>"#,
                    escape_text(&name)
                );
                Ok::<Bytes, SoapFault>(Bytes::from(xml))
            }),
        )
        .path("/hello")
        .build()
        .expect("WSDL build failed");

    let router = svc.into_router();
    let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await.unwrap();
    axum::serve(listener, router).await.unwrap();
}
fn parse_name(_b: &bytes::Bytes) -> Option<String> { None }

Try it

Send the SayHello request (SOAP 1.2 — the version is auto-detected):

curl -s http://localhost:8080/hello \
  -H 'Content-Type: application/soap+xml' \
  -d '<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope">
        <s:Body>
          <SayHello xmlns="urn:example:hello"><Name>Ada</Name></SayHello>
        </s:Body>
      </s:Envelope>'

You get back your SayHelloResponse element, wrapped in a SOAP 1.2 envelope by the library:

<env:Envelope xmlns:env="http://www.w3.org/2003/05/soap-envelope"><env:Body>
  <SayHelloResponse xmlns="urn:example:hello"><Greeting>Hello, Ada!</Greeting></SayHelloResponse>
</env:Body></env:Envelope>

Fetch the contract itself with a GET — every service path also serves its WSDL, with the <soap:address> rewritten to the request URL:

curl -s 'http://localhost:8080/hello?wsdl'

Step-by-step

1. ServerBuilder::from_wsdl_file

ServerBuilder::from_wsdl_file("path/to/service.wsdl") loads and parses the WSDL at the given filesystem path. The builder also accepts raw WSDL bytes or a custom WsdlLoader implementation for resolving imports over other transports.

2. .handler("MyOperation", ...)

.handler registers an async operation handler. The first argument is the operation name exactly as it appears in the WSDL <operation> element. Registering a name that is not in the WSDL causes .build() to return an error.

The second argument is any value that implements SoapHandler. The FnHandler wrapper converts a closure Fn(Bytes) -> Future<Output = Result<Bytes, SoapFault>> into a SoapHandler without you needing to implement the trait manually.

The Bytes argument your closure receives is the SOAP Body’s first child element as self-contained XML — all ancestor namespace declarations are re-emitted on the fragment root, so you can parse it independently.

Return Ok(Bytes) containing the response body element XML (without an enclosing envelope — the library adds the envelope), or return Err(SoapFault) to send a SOAP Fault.

3. .build()

Parses the WSDL, validates that every registered operation name exists, builds the dispatch table, and returns a Result<SoapService, BuildError>. Fail fast: call .expect or handle the error at startup.

4. svc.into_router()

Converts the built SoapService into an axum::Router mounted at the URL derived from the WSDL <service><port address> element. The router is a standard axum Router and can be nested or merged with other routers.

5. axum::serve

Standard axum server startup. The library has no opinion on TLS termination, timeouts, or other middleware — compose those layers on top of the returned Router before calling axum::serve.

Implementing SoapHandler directly

For handlers that need access to SOAP header fragments (e.g. WS-Addressing), implement the SoapHandler trait directly and override handle_with_headers:

#![allow(unused)]
fn main() {
use soap_server::{SoapHandler, SoapFault};
use bytes::Bytes;
use async_trait::async_trait;

struct MyHandler;

#[async_trait]
impl SoapHandler for MyHandler {
    async fn handle(&self, body: Bytes) -> Result<Bytes, SoapFault> {
        Ok(Bytes::from(r#"<MyResponse xmlns="urn:example"/>"#))
    }

    async fn handle_with_headers(
        &self,
        body: Bytes,
        headers: &[Bytes],
    ) -> Result<Bytes, SoapFault> {
        // Each element of `headers` is the raw bytes of one direct child of <Header>.
        let _ = headers; // inspect as needed
        self.handle(body).await
    }
}
}