When you bridge SOAP services to REST consumers, one of the hardest translation problems is mapping SOAP Faults to appropriate HTTP status codes. SOAP uses its own fault code system that does not align neatly with HTTP semantics, and getting this mapping wrong leads to confusing error responses that make debugging harder for downstream developers.
This guide provides a complete, opinionated mapping for both SOAP 1.1 and SOAP 1.2 Fault codes, explains the reasoning behind each mapping, and shows how to extract useful error information from SOAP Fault detail elements.
SOAP Fault Structure: 1.1 vs 1.2
Before diving into mappings, it helps to understand what SOAP Faults look like in both versions.
SOAP 1.1 Fault
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<soap:Fault>
<faultcode>soap:Client</faultcode>
<faultstring>Invalid account number format</faultstring>
<faultactor>https://payments.example.com/soap</faultactor>
<detail>
<tns:ValidationError xmlns:tns="http://example.com/errors">
<tns:field>accountNumber</tns:field>
<tns:value>ABC-INVALID</tns:value>
<tns:rule>Must match pattern: \d{10}</tns:rule>
</tns:ValidationError>
</detail>
</soap:Fault>
</soap:Body>
</soap:Envelope>
SOAP 1.1 Fault elements:
| Element | Required | Description |
|---|---|---|
faultcode | Yes | Qualified name identifying the fault class |
faultstring | Yes | Human-readable error description |
faultactor | No | URI identifying who caused the fault |
detail | No | Application-specific error details (only for Body faults) |
SOAP 1.2 Fault
<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:Subcode>
<soap:Value>tns:InvalidInput</soap:Value>
</soap:Subcode>
</soap:Code>
<soap:Reason>
<soap:Text xml:lang="en">
Invalid account number format
</soap:Text>
</soap:Reason>
<soap:Node>https://payments.example.com/soap</soap:Node>
<soap:Detail>
<tns:ValidationError xmlns:tns="http://example.com/errors">
<tns:field>accountNumber</tns:field>
<tns:value>ABC-INVALID</tns:value>
<tns:rule>Must match pattern: \d{10}</tns:rule>
</tns:ValidationError>
</soap:Detail>
</soap:Fault>
</soap:Body>
</soap:Envelope>
SOAP 1.2 introduced a hierarchical Code/Subcode structure and supports multiple language Reason texts. The terminology also changed: Client became Sender and Server became Receiver.
SOAP 1.1 Fault Code to HTTP Status Mapping
Standard Fault Codes
| SOAP 1.1 faultcode | HTTP Status | Reasoning |
|---|---|---|
soap:Client | 400 Bad Request | Client sent malformed or invalid data |
soap:Server | 502 Bad Gateway | Upstream SOAP server failed to process |
soap:VersionMismatch | 400 Bad Request | Client used wrong SOAP version |
soap:MustUnderstand | 501 Not Implemented | Server cannot process a required header |
Detailed Client Fault Mappings
The soap:Client fault code is a broad category. In practice, the faultstring and detail elements contain clues that allow more specific HTTP status mapping:
| faultstring Pattern | HTTP Status | When to Use |
|---|---|---|
| "Authentication failed" / "Invalid credentials" | 401 Unauthorized | Missing or invalid auth |
| "Access denied" / "Not authorized" | 403 Forbidden | Valid auth but insufficient permissions |
| "Resource not found" / "No such account" | 404 Not Found | Referenced entity does not exist |
| "Duplicate request" / "Already exists" | 409 Conflict | Resource state conflict |
| "Input validation failed" / "Invalid format" | 400 Bad Request | Malformed input data |
| "Rate limit exceeded" / "Too many requests" | 429 Too Many Requests | Throttling applied |
| "Request too large" / "Maximum size exceeded" | 413 Payload Too Large | Input exceeds size limits |
Detailed Server Fault Mappings
| faultstring Pattern | HTTP Status | When to Use |
|---|---|---|
| "Internal error" / "Unexpected exception" | 502 Bad Gateway | Generic upstream failure |
| "Service unavailable" / "System maintenance" | 503 Service Unavailable | Temporary upstream outage |
| "Timeout" / "Downstream timeout" | 504 Gateway Timeout | Upstream took too long |
| "Database error" / "Connection pool exhausted" | 502 Bad Gateway | Upstream infrastructure failure |
SOAP 1.2 Fault Code to HTTP Status Mapping
Standard Fault Codes
| SOAP 1.2 Code | HTTP Status | Reasoning |
|---|---|---|
soap:Sender | 400 Bad Request | Client-side error |
soap:Receiver | 502 Bad Gateway | Server-side error |
soap:VersionMismatch | 400 Bad Request | Wrong SOAP version in envelope |
soap:MustUnderstand | 501 Not Implemented | Unprocessable mandatory header |
soap:DataEncodingUnknown | 400 Bad Request | Unsupported encoding |
Subcode Mappings
SOAP 1.2 Subcodes provide finer-grained error classification:
| Subcode (common patterns) | HTTP Status | Description |
|---|---|---|
tns:AuthenticationRequired | 401 Unauthorized | Credentials missing |
tns:AuthenticationFailed | 401 Unauthorized | Credentials invalid |
tns:AuthorizationFailed | 403 Forbidden | Insufficient permissions |
tns:ResourceNotFound | 404 Not Found | Entity does not exist |
tns:ValidationFailed | 422 Unprocessable Entity | Semantically invalid input |
tns:ConcurrencyConflict | 409 Conflict | Optimistic lock failure |
tns:QuotaExceeded | 429 Too Many Requests | Usage limit reached |
tns:ServiceUnavailable | 503 Service Unavailable | Temporary outage |
Complete Mapping Table
This is the consolidated reference table for both SOAP versions:
| SOAP 1.1 | SOAP 1.2 | HTTP | Category |
|---|---|---|---|
soap:Client | soap:Sender | 400 | Generic client error |
soap:Client + auth failure | soap:Sender + AuthenticationFailed | 401 | Authentication |
soap:Client + access denied | soap:Sender + AuthorizationFailed | 403 | Authorization |
soap:Client + not found | soap:Sender + ResourceNotFound | 404 | Missing resource |
soap:Client + conflict | soap:Sender + ConcurrencyConflict | 409 | State conflict |
soap:Client + validation | soap:Sender + ValidationFailed | 422 | Semantic error |
soap:Client + rate limit | soap:Sender + QuotaExceeded | 429 | Throttling |
soap:Server | soap:Receiver | 502 | Generic server error |
soap:Server + unavailable | soap:Receiver + ServiceUnavailable | 503 | Temp outage |
soap:Server + timeout | soap:Receiver + Timeout | 504 | Upstream timeout |
soap:VersionMismatch | soap:VersionMismatch | 400 | Protocol error |
soap:MustUnderstand | soap:MustUnderstand | 501 | Unsupported feature |
Extracting Error Information from SOAP Fault Detail
The detail element is where the most useful debugging information lives, but its structure is entirely service-specific. Here is how to extract it programmatically.
JavaScript/TypeScript
import { XMLParser } from "fast-xml-parser";
interface SoapFaultInfo {
httpStatus: number;
code: string;
message: string;
detail: Record<string, unknown> | null;
}
function parseSoapFault(xmlBody: string): SoapFaultInfo | null {
const parser = new XMLParser({
ignoreAttributes: false,
removeNSPrefix: true,
});
const parsed = parser.parse(xmlBody);
const fault =
parsed?.Envelope?.Body?.Fault;
if (!fault) return null;
// SOAP 1.1
const faultcode = fault.faultcode ?? fault.Code?.Value;
const faultstring = fault.faultstring ?? fault.Reason?.Text;
const detail = fault.detail ?? fault.Detail ?? null;
const httpStatus = mapFaultCodeToHttp(faultcode, faultstring);
return {
httpStatus,
code: faultcode,
message: typeof faultstring === "string"
? faultstring
: faultstring?.["#text"] ?? "Unknown SOAP error",
detail,
};
}
function mapFaultCodeToHttp(
faultcode: string,
faultstring: string
): number {
const message = (faultstring ?? "").toLowerCase();
if (faultcode?.includes("Client") || faultcode?.includes("Sender")) {
if (message.includes("authentication") || message.includes("credentials")) return 401;
if (message.includes("authorized") || message.includes("access denied")) return 403;
if (message.includes("not found")) return 404;
if (message.includes("conflict") || message.includes("duplicate")) return 409;
if (message.includes("rate limit") || message.includes("quota")) return 429;
return 400;
}
if (faultcode?.includes("Server") || faultcode?.includes("Receiver")) {
if (message.includes("unavailable") || message.includes("maintenance")) return 503;
if (message.includes("timeout")) return 504;
return 502;
}
if (faultcode?.includes("VersionMismatch")) return 400;
if (faultcode?.includes("MustUnderstand")) return 501;
return 502; // Default: treat as upstream error
}
Python
import xml.etree.ElementTree as ET
from dataclasses import dataclass
from typing import Optional
@dataclass
class SoapFaultInfo:
http_status: int
code: str
message: str
detail: Optional[str]
def parse_soap_fault(xml_body: bytes) -> Optional[SoapFaultInfo]:
root = ET.fromstring(xml_body)
# Try SOAP 1.1 namespace first, then 1.2
for ns in [
"http://schemas.xmlsoap.org/soap/envelope/",
"http://www.w3.org/2003/05/soap-envelope",
]:
fault = root.find(f".//{{{ns}}}Fault")
if fault is not None:
break
else:
return None
# Extract fault code and message (handle both 1.1 and 1.2)
faultcode = (
fault.findtext("faultcode") # 1.1
or fault.findtext(
f"{{{ns}}}Code/{{{ns}}}Value"
) # 1.2
or "Unknown"
)
faultstring = (
fault.findtext("faultstring") # 1.1
or fault.findtext(
f"{{{ns}}}Reason/{{{ns}}}Text"
) # 1.2
or "Unknown error"
)
# Extract detail
detail_el = fault.find("detail") or fault.find(f"{{{ns}}}Detail")
detail_text = (
ET.tostring(detail_el, encoding="unicode")
if detail_el is not None
else None
)
http_status = map_fault_code_to_http(faultcode, faultstring)
return SoapFaultInfo(
http_status=http_status,
code=faultcode,
message=faultstring.strip(),
detail=detail_text,
)
def map_fault_code_to_http(faultcode: str, faultstring: str) -> int:
message = faultstring.lower()
if "Client" in faultcode or "Sender" in faultcode:
if "authentication" in message or "credentials" in message:
return 401
if "authorized" in message or "access denied" in message:
return 403
if "not found" in message:
return 404
if "conflict" in message or "duplicate" in message:
return 409
if "rate limit" in message or "quota" in message:
return 429
return 400
if "Server" in faultcode or "Receiver" in faultcode:
if "unavailable" in message or "maintenance" in message:
return 503
if "timeout" in message:
return 504
return 502
if "VersionMismatch" in faultcode:
return 400
if "MustUnderstand" in faultcode:
return 501
return 502
Building REST Error Responses from SOAP Faults
Once you have parsed the SOAP Fault and mapped it to an HTTP status, format the REST error response consistently:
{
"error": {
"type": "soap_fault",
"code": "AUTHENTICATION_FAILED",
"message": "Invalid credentials for the upstream SOAP service",
"detail": {
"soap_fault_code": "soap:Client",
"soap_fault_string": "Authentication failed: invalid username or password",
"upstream_service": "PaymentService"
}
}
}
Guidelines for the REST error response:
- Always include the original SOAP fault code in the detail — downstream developers may need it for debugging with the upstream service provider
- Sanitize sensitive information — never expose internal server paths, stack traces, or database details from the SOAP Fault detail element
- Use consistent error codes — map SOAP faults to your own error code taxonomy (e.g.,
AUTHENTICATION_FAILED,VALIDATION_ERROR,UPSTREAM_UNAVAILABLE) - Include the upstream service name — when proxying multiple SOAP services, identify which one produced the error
Custom Fault Handling
Many SOAP services define custom fault types in their WSDL. These appear in the detail element and require service-specific parsing.
Example: a payment service that defines its own error structure:
<soap:Fault>
<faultcode>soap:Client</faultcode>
<faultstring>Payment processing failed</faultstring>
<detail>
<pay:PaymentError xmlns:pay="http://example.com/payments">
<pay:errorCode>INSUFFICIENT_FUNDS</pay:errorCode>
<pay:availableBalance>45.00</pay:availableBalance>
<pay:requestedAmount>100.00</pay:requestedAmount>
<pay:currency>USD</pay:currency>
</pay:PaymentError>
</detail>
</soap:Fault>
For this fault, the appropriate REST response would be:
{
"error": {
"type": "payment_error",
"code": "INSUFFICIENT_FUNDS",
"message": "Payment processing failed: insufficient funds",
"detail": {
"available_balance": 45.00,
"requested_amount": 100.00,
"currency": "USD"
}
}
}
The HTTP status should be 422 Unprocessable Entity (the request was valid but the business logic rejected it) rather than 400 (which implies a malformed request).
Edge Cases and Pitfalls
1. HTTP 200 with SOAP Fault
Many SOAP services return HTTP 200 even when the response contains a SOAP Fault. Your proxy must always parse the response body to detect faults, not just check the HTTP status:
// Wrong — misses faults returned with HTTP 200
if (response.status === 200) {
return parseSuccessResponse(response.data);
}
// Correct — always check for faults in the body
const fault = parseSoapFault(response.data);
if (fault) {
return buildErrorResponse(fault);
}
return parseSuccessResponse(response.data);
2. SOAP 1.2 HTTP Status Codes
SOAP 1.2 specifies that Sender faults should return HTTP 400 and Receiver faults should return HTTP 500. However, not all implementations follow this. Always parse the XML body.
3. Empty Detail Elements
Some services return a detail element that is empty or contains only whitespace. Treat these as having no detail rather than failing to parse:
detail_el = fault.find("detail")
if detail_el is not None and detail_el.text and detail_el.text.strip():
# Has meaningful detail
pass
elif detail_el is not None and len(detail_el) > 0:
# Has child elements
pass
else:
# No useful detail
pass
4. Multiple Fault Subcodes
SOAP 1.2 allows nested Subcodes. Walk the chain to find the most specific one:
def get_deepest_subcode(code_element, ns):
"""Traverse nested Subcodes to find the most specific code."""
current = code_element
deepest_value = current.findtext(f"{{{ns}}}Value", default="")
subcode = current.find(f"{{{ns}}}Subcode")
while subcode is not None:
value = subcode.findtext(f"{{{ns}}}Value")
if value:
deepest_value = value
subcode = subcode.find(f"{{{ns}}}Subcode")
return deepest_value
How SOAPless Helps
Implementing correct SOAP Fault to HTTP status mapping is tedious and error-prone. Every SOAP service has its own fault patterns, custom detail structures, and edge cases around HTTP status codes.
SOAPless handles this translation automatically. When an upstream SOAP service returns a Fault, SOAPless parses the fault code, analyzes the fault string and detail elements, and returns a properly mapped HTTP status code with a clean JSON error response. The mapping logic covers both SOAP 1.1 and 1.2, handles the HTTP 200-with-Fault edge case, and extracts structured information from custom detail elements — so your REST consumers get consistent, well-formed error responses without you writing or maintaining any fault-parsing code.