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
| Crate | Carries | Acknowledgement |
|---|---|---|
hl7-2-soap | An HL7 v2 message — v2.xml, or ER7 wrapped in one | A Status element, or the HL7 code |
hl7-3-soap | A complete HL7 v3 message, root element named for the interaction | The 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-2and thehl7-2-from-*crates own those. - No RIM decoding, in the v3 crate's case — that is
hl7-3, whichhl7-3-soapdeliberately does not depend on.
Receiving
HL7 v2
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
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
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
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
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 momentReading 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:
- A non-2xx HTTP status.
- A
Faultelement — even under HTTP 200, because some stacks answer 200 and put the refusal in the body. - The acknowledgement: a
Statuselement for v2, oracknowledgement/typeCode/@codefor v3. - 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, withacknowledgement/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.