soap-errorsrest-apireferenceweb-services

SOAP Fault to HTTP Status Code Mapping: A Developer's Reference Guide

SOAPless Team11 min read

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:

ElementRequiredDescription
faultcodeYesQualified name identifying the fault class
faultstringYesHuman-readable error description
faultactorNoURI identifying who caused the fault
detailNoApplication-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 faultcodeHTTP StatusReasoning
soap:Client400 Bad RequestClient sent malformed or invalid data
soap:Server502 Bad GatewayUpstream SOAP server failed to process
soap:VersionMismatch400 Bad RequestClient used wrong SOAP version
soap:MustUnderstand501 Not ImplementedServer 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 PatternHTTP StatusWhen to Use
"Authentication failed" / "Invalid credentials"401 UnauthorizedMissing or invalid auth
"Access denied" / "Not authorized"403 ForbiddenValid auth but insufficient permissions
"Resource not found" / "No such account"404 Not FoundReferenced entity does not exist
"Duplicate request" / "Already exists"409 ConflictResource state conflict
"Input validation failed" / "Invalid format"400 Bad RequestMalformed input data
"Rate limit exceeded" / "Too many requests"429 Too Many RequestsThrottling applied
"Request too large" / "Maximum size exceeded"413 Payload Too LargeInput exceeds size limits

Detailed Server Fault Mappings

faultstring PatternHTTP StatusWhen to Use
"Internal error" / "Unexpected exception"502 Bad GatewayGeneric upstream failure
"Service unavailable" / "System maintenance"503 Service UnavailableTemporary upstream outage
"Timeout" / "Downstream timeout"504 Gateway TimeoutUpstream took too long
"Database error" / "Connection pool exhausted"502 Bad GatewayUpstream infrastructure failure

SOAP 1.2 Fault Code to HTTP Status Mapping

Standard Fault Codes

SOAP 1.2 CodeHTTP StatusReasoning
soap:Sender400 Bad RequestClient-side error
soap:Receiver502 Bad GatewayServer-side error
soap:VersionMismatch400 Bad RequestWrong SOAP version in envelope
soap:MustUnderstand501 Not ImplementedUnprocessable mandatory header
soap:DataEncodingUnknown400 Bad RequestUnsupported encoding

Subcode Mappings

SOAP 1.2 Subcodes provide finer-grained error classification:

Subcode (common patterns)HTTP StatusDescription
tns:AuthenticationRequired401 UnauthorizedCredentials missing
tns:AuthenticationFailed401 UnauthorizedCredentials invalid
tns:AuthorizationFailed403 ForbiddenInsufficient permissions
tns:ResourceNotFound404 Not FoundEntity does not exist
tns:ValidationFailed422 Unprocessable EntitySemantically invalid input
tns:ConcurrencyConflict409 ConflictOptimistic lock failure
tns:QuotaExceeded429 Too Many RequestsUsage limit reached
tns:ServiceUnavailable503 Service UnavailableTemporary outage

Complete Mapping Table

This is the consolidated reference table for both SOAP versions:

SOAP 1.1SOAP 1.2HTTPCategory
soap:Clientsoap:Sender400Generic client error
soap:Client + auth failuresoap:Sender + AuthenticationFailed401Authentication
soap:Client + access deniedsoap:Sender + AuthorizationFailed403Authorization
soap:Client + not foundsoap:Sender + ResourceNotFound404Missing resource
soap:Client + conflictsoap:Sender + ConcurrencyConflict409State conflict
soap:Client + validationsoap:Sender + ValidationFailed422Semantic error
soap:Client + rate limitsoap:Sender + QuotaExceeded429Throttling
soap:Serversoap:Receiver502Generic server error
soap:Server + unavailablesoap:Receiver + ServiceUnavailable503Temp outage
soap:Server + timeoutsoap:Receiver + Timeout504Upstream timeout
soap:VersionMismatchsoap:VersionMismatch400Protocol error
soap:MustUnderstandsoap:MustUnderstand501Unsupported 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:

  1. Always include the original SOAP fault code in the detail — downstream developers may need it for debugging with the upstream service provider
  2. Sanitize sensitive information — never expose internal server paths, stack traces, or database details from the SOAP Fault detail element
  3. Use consistent error codes — map SOAP faults to your own error code taxonomy (e.g., AUTHENTICATION_FAILED, VALIDATION_ERROR, UPSTREAM_UNAVAILABLE)
  4. 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.