TestingAPIs is still being built. If something breaks, email support@testingapis.com.

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:

Terminal
$ 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>
This example uses endpoints for signed-in members.Sign inCreate a free account
  • soap:Fault replaces the normal response inside the Body. A response contains either a result or a fault, never both.
  • faultcode says which kind of error it is. The value is a qualified name: soap:Server means Server in the SOAP envelope namespace, using whatever prefix the document declared.
  • faultstring is 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 codeMeans
ClientThe request was wrong: bad or missing data. Sending it again unchanged will fail again.
ServerThe request was fine, but the server couldn't process it. It may work later.
VersionMismatchThe envelope's namespace isn't one the server understands.
MustUnderstandA header marked as required wasn't understood by the server.

A fault in SOAP 1.2

A value that isn't a number:

Terminal
$ 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>
This example uses endpoints for signed-in members.Sign inCreate a free account

The same idea, rearranged and renamed:

SOAP 1.1SOAP 1.2
The codefaultcode, in no namespaceCode/Value, in the envelope namespace
The messagefaultstringReason/Text, with a language attribute; there can be one per language
Bad requestClientSender
Server problemServerReceiver
More detailAn optional detail elementAn optional Detail element, and optional Subcode under Code
HTTP statusAlways 500400 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:

RequestCodeStatusfaultstring
Divide by zeroServer500Attempted to divide by zero.
intA is "five"Client500Element 'intA' is missing or is not a valid integer.
Same, in SOAP 1.2Sender400(the same text, in Reason)
Divide by zero, in SOAP 1.2Receiver500Attempted to divide by zero.
2147483647 × 2Client500Arithmetic operation resulted in an overflow of a 32-bit integer.
SOAPAction says Subtract, body says AddClient500SOAPAction 'http://tempuri.org/Subtract' does not match body operation 'Add'.
A 1.2 envelope sent as text/xmlVersionMismatch500Envelope namespace '...' does not match the Content-Type ...
Envelope not closedClient500Malformed XML: Unexpected end of file has occurred. ...
countryCode XXClient500Country code 'XX' not found.
Add in the wrong namespaceClient500Operation '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:

Soap.java
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.

FaultsTest.java
@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());
}
This example uses endpoints for signed-in members.Sign inCreate a free account
FaultsTest.java
@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());
}
This example uses endpoints for signed-in members.Sign inCreate a free account
FaultsTest.java
@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());
}
This example uses endpoints for signed-in members.Sign inCreate a free account

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:

FaultsTest.java
@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()));
}
This example uses endpoints for signed-in members.Sign inCreate a free account

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. 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. 2.What are the SOAP 1.2 names for Client and Server?Show answer

    Sender and Receiver.

  3. 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. 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. 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.

Practise and go further