SOAP faults
How errors come back in each version, which side is to blame, and the status codes that go with them.
When a SOAP call goes wrong, the server doesn't invent its own error format. It answers with a fault, a standard element inside the Body that says what kind of error happened and why. Every SOAP client understands faults, which is one of SOAP's real strengths. The catch is that the two versions of SOAP write faults differently, and pair them with different HTTP status codes. This lesson covers both, and how to test for them.
A fault in SOAP 1.1
Dividing by zero:
$ curl -i -X POST https://api.testingapis.com/api/soap/calculator \
-H "Content-Type: text/xml" \
-d '<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Body><Divide xmlns="http://tempuri.org/"><intA>5</intA><intB>0</intB></Divide></soap:Body></soap:Envelope>'
HTTP/1.1 500 Internal Server Error
content-type: text/xml; charset=utf-8
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<soap:Fault>
<faultcode>soap:Server</faultcode>
<faultstring>Attempted to divide by zero.</faultstring>
</soap:Fault>
</soap:Body>
</soap:Envelope>soap:Faultreplaces the normal response inside the Body. A response contains either a result or a fault, never both.faultcodesays which kind of error it is. The value is a qualified name:soap:ServermeansServerin the SOAP envelope namespace, using whatever prefix the document declared.faultstringis a human-readable explanation.- The HTTP status is 500. The SOAP 1.1 specification says faults are sent with 500, whatever caused them.
One detail trips up XPath: faultcode and faultstringhave no prefix and there's no default namespace in scope, so they're in no namespace at all, unlike Fault around them. A query for soap:faultcode finds nothing.
| SOAP 1.1 code | Means |
|---|---|
| Client | The request was wrong: bad or missing data. Sending it again unchanged will fail again. |
| Server | The request was fine, but the server couldn't process it. It may work later. |
| VersionMismatch | The envelope's namespace isn't one the server understands. |
| MustUnderstand | A header marked as required wasn't understood by the server. |
A fault in SOAP 1.2
A value that isn't a number:
$ curl -i -X POST https://api.testingapis.com/api/soap/calculator \
-H "Content-Type: application/soap+xml" \
-d '<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope"><soap:Body><Add xmlns="http://tempuri.org/"><intA>five</intA><intB>3</intB></Add></soap:Body></soap:Envelope>'
HTTP/1.1 400 Bad Request
content-type: application/soap+xml; charset=utf-8
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope">
<soap:Body>
<soap:Fault>
<soap:Code>
<soap:Value>soap:Sender</soap:Value>
</soap:Code>
<soap:Reason>
<soap:Text xml:lang="en">Element 'intA' is missing or is not a valid integer.</soap:Text>
</soap:Reason>
</soap:Fault>
</soap:Body>
</soap:Envelope>The same idea, rearranged and renamed:
| SOAP 1.1 | SOAP 1.2 | |
|---|---|---|
| The code | faultcode, in no namespace | Code/Value, in the envelope namespace |
| The message | faultstring | Reason/Text, with a language attribute; there can be one per language |
| Bad request | Client | Sender |
| Server problem | Server | Receiver |
| More detail | An optional detail element | An optional Detail element, and optional Subcode under Code |
| HTTP status | Always 500 | 400 for Sender faults, 500 for the others |
The last row is the important one. SOAP 1.2's HTTP binding maps a Sender fault to 400 Bad Request, which matches what you'd expect from REST. SOAP 1.1 sends 500 even when the client is to blame. A tester used to REST sees a 500 and files a server bug, when the fault code says the request was wrong.
Every fault this service can return
Each of these was sent to the calculator or countries service. The status codes are for SOAP 1.1 unless stated:
| Request | Code | Status | faultstring |
|---|---|---|---|
| Divide by zero | Server | 500 | Attempted to divide by zero. |
| intA is "five" | Client | 500 | Element 'intA' is missing or is not a valid integer. |
| Same, in SOAP 1.2 | Sender | 400 | (the same text, in Reason) |
| Divide by zero, in SOAP 1.2 | Receiver | 500 | Attempted to divide by zero. |
| 2147483647 × 2 | Client | 500 | Arithmetic operation resulted in an overflow of a 32-bit integer. |
| SOAPAction says Subtract, body says Add | Client | 500 | SOAPAction 'http://tempuri.org/Subtract' does not match body operation 'Add'. |
| A 1.2 envelope sent as text/xml | VersionMismatch | 500 | Envelope namespace '...' does not match the Content-Type ... |
| Envelope not closed | Client | 500 | Malformed XML: Unexpected end of file has occurred. ... |
| countryCode XX | Client | 500 | Country code 'XX' not found. |
| Add in the wrong namespace | Client | 500 | Operation 'Add' must be in namespace 'http://tempuri.org/', but it is in ... |
Two things to notice. Divide by zero is a Serverfault here, though arguably the client sent bad input; services don't always classify the way you would, so test what they document. And a request that isn't SOAP at all, such as a JSON body, gets 415 Unsupported Media Type with a JSON message, not a fault, because the server never got as far as reading an envelope.
Reading a fault in code
The project's Soap.faultreads either version's layout and returns the code and message, or null for a normal response:
public static Fault fault(Document doc) {
XmlQuery q = new XmlQuery(Map.of("s11", SOAP11, "s12", SOAP12));
// SOAP 1.1: faultcode and faultstring are plain elements with no namespace.
if (q.bool(doc, "boolean(/s11:Envelope/s11:Body/s11:Fault)")) {
return new Fault(q.string(doc, "/s11:Envelope/s11:Body/s11:Fault/faultcode"),
q.string(doc, "/s11:Envelope/s11:Body/s11:Fault/faultstring"));
}
// SOAP 1.2: everything is in the envelope namespace, and the code sits inside Code/Value.
if (q.bool(doc, "boolean(/s12:Envelope/s12:Body/s12:Fault)")) {
return new Fault(q.string(doc, "/s12:Envelope/s12:Body/s12:Fault/s12:Code/s12:Value"),
q.string(doc, "/s12:Envelope/s12:Body/s12:Fault/s12:Reason/s12:Text"));
}
return null;
}Notice faultcode without a prefix in the 1.1 query, for the reason above. The Fault record's codeName() drops the prefix from the code, so tests compare Server rather than soap:Server, which would break if the server chose another prefix. That's a shortcut: strictly, the prefix should be resolved to its namespace, but for the standard codes the local name is enough.
Testing faults
A fault test checks three things: the HTTP status, the fault code, and the message.
@Test
@DisplayName("SOAP 1.1: dividing by zero is a Server fault, sent with status 500")
void serverFault11() {
Api.Response response = Soap.call("/api/soap/calculator", SOAP_1_1, null, divide(SOAP_1_1, "5", "0"));
assertEquals(500, response.status());
Soap.Fault fault = Soap.fault(response.xml());
assertEquals("Server", fault.codeName());
assertEquals("Attempted to divide by zero.", fault.reason());
}@Test
@DisplayName("SOAP 1.2: bad input is a Sender fault, sent with status 400")
void senderFault12() {
Api.Response response = Soap.call("/api/soap/calculator", SOAP_1_2, null, divide(SOAP_1_2, "five", "1"));
assertEquals(400, response.status());
Soap.Fault fault = Soap.fault(response.xml());
assertEquals("Sender", fault.codeName());
assertEquals("Element 'intA' is missing or is not a valid integer.", fault.reason());
}@Test
@DisplayName("a 1.2 envelope sent as 1.1 is a VersionMismatch fault")
void versionMismatch() {
Api.Response response = Soap.call("/api/soap/calculator", SOAP_1_1, null, divide(SOAP_1_2, "6", "3"));
assertEquals(500, response.status());
assertEquals("VersionMismatch", Soap.fault(response.xml()).codeName());
}divide(SOAP_1_2, ...) builds a 1.2 envelope, and Soap.call(..., SOAP_1_1, ...) sends it with the 1.1 content type, so the two disagree. And the counterpart every fault suite needs, that a good request is not a fault:
@Test
@DisplayName("a successful response has no fault")
void noFault() {
Api.Response response = Soap.call("/api/soap/calculator", SOAP_1_1, CALC + "Divide", divide(SOAP_1_1, "6", "3"));
assertEquals(200, response.status());
assertNull(Soap.fault(response.xml()));
}What to check in any SOAP service
- Each kind of bad input gives a Client or Sender fault, never a result computed from garbage.
- The HTTP status matches the version: 500 for every 1.1 fault, 400 or 500 for 1.2.
- The fault is in the same SOAP version as the request.
- The message helps the caller fix the request, and doesn't reveal internals such as stack traces, file paths, class names or library settings.
- Faults are valid SOAP: a client must be able to parse them like any other response.
Check your understanding
1.A SOAP 1.1 service returns HTTP 500. Does that mean the server is at fault?Show answer
Not necessarily. SOAP 1.1 sends every fault with 500. Read faultcode: Client means the request was wrong, Server means the server failed.
2.What are the SOAP 1.2 names for Client and Server?Show answer
Sender and Receiver.
3.Which HTTP status goes with a SOAP 1.2 Sender fault?Show answer
400 Bad Request. Receiver faults, and the other codes, use 500.
4.Why does the XPath /s11:Envelope/s11:Body/s11:Fault/s11:faultcode find nothing?Show answer
In SOAP 1.1, faultcode and faultstring are in no namespace. Query faultcode without a prefix.
5.Why compare the fault code's local name rather than the text soap:Server?Show answer
The prefix is chosen by the server and can change; the local name and namespace are what identify the code.
Exercise
Write a parameterized test that sends each bad request from the table above in both SOAP versions and checks the code and status for each: Client and 500 in 1.1, Sender and 400 in 1.2 for the bad-input cases. Then send a GetCountryInfo with an empty countryCode element and check what comes back.