SOAP faults and error handling
A SOAP fault inside a 500 response is still a structured error message, and mapping it to the right client exception takes a deliberate decision.
The fault structure
<!-- SOAP 1.1 -->
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<soap:Fault>
<faultcode>soap:Client</faultcode>
<faultstring>SKU A-1 is not available</faultstring>
<faultactor>urn:example:orders</faultactor>
<detail>
<OrderFault xmlns="urn:example:orders">
<code>SKU_UNAVAILABLE</code>
<sku>A-1</sku>
</OrderFault>
</detail>
</soap:Fault>
</soap:Body>
</soap:Envelope><!-- SOAP 1.2 -->
<env:Envelope xmlns:env="http://www.w3.org/2003/05/soap-envelope">
<env:Body>
<env:Fault>
<env:Code>
<env:Value>env:Sender</env:Value>
<env:Subcode><env:Value>rpc:BadArguments</env:Value></env:Subcode>
</env:Code>
<env:Reason>
<env:Text xml:lang="en">SKU A-1 is not available</env:Text>
</env:Reason>
<env:Detail>
<OrderFault xmlns="urn:example:orders"><code>SKU_UNAVAILABLE</code></OrderFault>
</env:Detail>
</env:Fault>
</env:Body>
</env:Envelope>| 1.1 faultcode | 1.2 Code/Value | Meaning | Client action |
|---|---|---|---|
VersionMismatch | env:VersionMismatch | Wrong envelope namespace | Fix the version, do not retry |
MustUnderstand | env:MustUnderstand | A required header was not processed | Fix the header, do not retry |
Client | env:Sender | The request is at fault | Do not retry as-is |
Server | env:Receiver | The service failed | Retry with backoff, then alert |
DataEncodingUnknown | env:DataEncodingUnknown | Unsupported encoding declaration | Fix the request |
Handling faults in client code
import javax.xml.ws.WebServiceException;
import javax.xml.ws.soap.SOAPFaultException;
import javax.xml.soap.SOAPFault;
import javax.xml.soap.Detail;
try {
port.placeOrder(order);
} catch (SOAPFaultException e) {
SOAPFault f = e.getFault();
String code = f.getFaultCode();
Detail detail = f.getDetail();
// Distinguish a retryable server fault from a permanent request error
if ("Server".equals(code) || "env:Receiver".equals(code)) {
throw new RetryableException(f.getFaultString(), e);
}
throw new InvalidRequestException(f.getFaultString(), detail, e);
} catch (WebServiceException e) {
// Transport level: DNS, TLS, timeout, connection reset
throw new RetryableException("transport failure", e);
}- A fault is a first-class application message, not merely an HTTP error. Preserve
faultcode,faultstringand the detail into your own error type. - Distinguish three layers: transport failure, SOAP protocol fault, and application error carried in the detail.
- A timeout is not a confirmed failure. A retried write after a timeout can duplicate the operation — the fix is an idempotency key, not blind retries.
- Application errors that are expected in normal operation are better modelled as a normal response with a status field than as a fault, which tooling treats as exceptional.
faultactoronly matters when an intermediary, such as a gateway, is reporting the fault.- Never surface a raw stack trace in
detail— it leaks internals and is often copied into logs by clients.
⚠️
Some frameworks throw a generic exception when the response body is a fault but the HTTP status is 200. If your error handling depends on the status code alone, those failures arrive as a parse error on a success path — always inspect the body of an unexpectedly shaped response.
FAQ
Should my service return a fault for validation errors?
For a SOAP contract, yes — a fault with a structured detail is the contract-conformant way to report a rejected request. Just make sure the detail is a documented schema type rather than a free-text string.
Why does the client see a NullPointerException instead of my fault?
Because the generated proxy could not map the detail element to the declared fault bean. Make sure the fault is declared in the WSDL for that operation and the detail matches the declared type exactly.
Related
Calling a SOAP service, and why REST usually wins Testing and debugging SOAP services
Last refreshed 2026-09-18.