Guides

SOAP over HTTP

For HL7 v2, SOAP is the transport an estate ends up with when the messages have to cross a boundary that speaks HTTP. For HL7 v3, SOAP is the transport. Two crates, deliberately the same shape.

Two crates, one shape

CrateCarriesAcknowledgement
hl7-2-soapAn HL7 v2 message — v2.xml, or ER7 wrapped in oneA Status element, or the HL7 code
hl7-3-soapA complete HL7 v3 message, root element named for the interactionThe real v3 MCCI_IN000002UV01

The difference in emphasis matters. For v2, MLLP is the usual transport and SOAP is the exception — what you get when the system at the far end was built by a team who had a WSDL and no socket. For v3 it is the other way round: v3 was designed alongside SOAP and WS-*, and real deployments — NHS England's Personal Demographics Service, IHE profiles built on v3 — carry it that way.

What they do, and what they leave to you

They do:

  • Parse a SOAP envelope and take the single business payload out of its body.
  • Faults, each carrying the HTTP status that belongs with it.
  • Read which message a payload is and its control ID, and check it against what the interface accepts.
  • Build the reply, and read one as accepted or rejected.
  • Serve a WSDL describing the endpoint at the address it was reached on.

They do not:

  • No HTTP client and no HTTP server. They turn bytes into meaning and back, and leave the socket to whatever you already use.
  • No HL7 validation and no format conversion. hl7-2 and the hl7-2-from-* crates own those.
  • No RIM decoding, in the v3 crate's case — that is hl7-3, which hl7-3-soap deliberately does not depend on.

Receiving

HL7 v2

rust
use hl7_2_soap::{Fault, message, response};

fn handle(request_body: &str) -> (u16, String) {
    match accept(request_body) {
        Ok(control_id) => (200, response::success(&control_id)),
        Err(fault) => (fault.status, fault.to_envelope()),
    }
}

fn accept(request_body: &str) -> Result<String, Fault> {
    let envelope = hl7_2_soap::parse(request_body)?;
    let payload = envelope.payload()?;
    message::check(payload, &["ADT_A05".to_string()], &[])?;
    // ...validate and forward the payload here...
    Ok(message::control_id(payload).unwrap_or_default().to_string())
}

Fault converts to an envelope and carries its own HTTP status, so the whole error path is two lines. message::check is where you say what this endpoint accepts; anything else is a fault rather than something that reaches your business logic.

HL7 v3 — the same shape

rust
use hl7_3_soap::{Fault, message, response};

fn accept(request_body: &str) -> Result<String, Fault> {
    let envelope = hl7_3_soap::parse(request_body)?;
    let payload = envelope.payload()?;
    message::check(payload, &["PRPA_IN201305UV02".to_string()], &[])?;
    // ...decode the payload with hl7-3, and forward it, here...
    Ok(message::control_id(payload).unwrap_or_default().to_string())
}

Sending

HL7 v2

rust
use hl7_2_soap::{message, response::{self, Outcome}};

let body = message::wrap_er7("MSH|^~\\&|APP||||1||ADT^A01|9|P|2.5");
// ...POST body with Content-Type: text/xml; charset=utf-8...

match response::evaluate(status, &reply) {
    Outcome::Accepted => {}
    Outcome::Rejected(reason) => eprintln!("not delivered: {reason}"),
}

HL7 v3

rust
use hl7_3_soap::{envelope, response::{self, Outcome}};

let body = envelope::wrap_xml(
    r#"<PRPA_IN201305UV02><id extension="9"/></PRPA_IN201305UV02>"#,
);
// ...POST body with Content-Type: text/xml; charset=utf-8...

match response::evaluate(status, &reply) {
    Outcome::Accepted => {}
    Outcome::Rejected(reason) => eprintln!("not delivered: {reason}"),
}

Note message::wrap_er7 in the v2 crate: an interface that speaks ER7 rather than v2.xml can still go over SOAP, with the pipe-delimited message carried inside the envelope.

Three things they are opinionated about

One payload per body

SOAP permits several; no HL7 interface means several. A body with none or many is a fault rather than a silent choice of the first child — because a silent choice is how you process the wrong message and never find out.

Prefixes do not matter

The same envelope arrives as soapenv:, soap:, SOAP-ENV:, or unprefixed depending on which stack sent it. Elements are matched on their local name, so all four are read alike. Insisting on one prefix is the single most common way a working SOAP integration breaks when the other end changes stack.

A fault carries its HTTP status

text
Client                  400   do not retry — the request is wrong
Client.Authorization    403   do not retry — the caller is not allowed
Server                  500   retry — the far side had a bad moment

Reading a response

There is no equivalent of MSA-1 in SOAP — there are three places to look and no agreement about which wins. response::evaluate reads all three, in the order that cannot be talked out of a rejection:

  1. A non-2xx HTTP status.
  2. A Fault element — even under HTTP 200, because some stacks answer 200 and put the refusal in the body.
  3. The acknowledgement: a Status element for v2, or acknowledgement/typeCode/@code for v3.
  4. Otherwise, acceptance.

In the v2 crate, Status is accepted for AA, CA, and Success. Both conventions are in the field — an implementation that echoes the HL7 acknowledgement code, and one that writes a word — and a sender that knows only one will retry forever against an endpoint that speaks the other. The crate this one was generalised from had exactly that split between its own receiver and its own sender.

In the v3 crate, AA and CA are accepted, matching v2's MSA-1: the two standards draw the acceptance codes from the same conceptual vocabulary.

WSDL

Both crates can serve a WSDL that describes the endpoint at the address it was reached on, rather than at a hardcoded one. That matters behind a load balancer or a reverse proxy, where the address a client used is not the address the process is bound to — a WSDL that advertises the wrong host sends the next client somewhere it cannot reach.

Where the v3 crate differs

  • The payload is a complete HL7 v3 message whose root element is named for the interaction — PRPA_IN201305UV02, say — rather than an HL7 v2 message in one of two encodings.
  • It can read the payload's claimed assigning authority, in addition to which interaction it is and its control ID.
  • It builds the real HL7 v3 acknowledgement, MCCI_IN000002UV01, with acknowledgement/typeCode — not an invented shape.
  • It has no dependency on hl7-3. A SOAP envelope is XML, and reading one requires no HL7 knowledge beyond the names of a few elements — so a router can move v3 traffic without decoding it.

Both crates depend on exactly one thing: hl7-2-xml-lite-helper, re-exported as xml, which has no dependencies of its own.