SOAP サービスと REST コンシューマーをブリッジする際に最も厄介な変換問題の1つが、SOAP Fault から適切な HTTP ステータスコードへのマッピングです。SOAP は独自の Fault コード体系を使用しており、HTTP のセマンティクスとは綺麗に対応しません。このマッピングを誤ると、下流の開発者にとってデバッグが困難になる混乱したエラーレスポンスが返されることになります。
このガイドでは、SOAP 1.1 と 1.2 の両方の Fault コードに対する完全なマッピング、各マッピングの理由、SOAP Fault の detail 要素から有用なエラー情報を抽出する方法を解説します。
SOAP Fault の構造: 1.1 vs 1.2
マッピングに入る前に、両バージョンの SOAP Fault がどのような構造かを確認しましょう。
SOAP 1.1 の Fault
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<soap:Fault>
<faultcode>soap:Client</faultcode>
<faultstring>口座番号の形式が不正です</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>10桁の数字である必要があります</tns:rule>
</tns:ValidationError>
</detail>
</soap:Fault>
</soap:Body>
</soap:Envelope>
SOAP 1.1 Fault の要素:
| 要素 | 必須 | 説明 |
|---|---|---|
faultcode | はい | Fault の分類を識別する修飾名 |
faultstring | はい | 人間が読めるエラー説明 |
faultactor | いいえ | Fault の原因となったノードの URI |
detail | いいえ | アプリケーション固有のエラー詳細 (Body の Fault のみ) |
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="ja">
口座番号の形式が不正です
</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>10桁の数字である必要があります</tns:rule>
</tns:ValidationError>
</soap:Detail>
</soap:Fault>
</soap:Body>
</soap:Envelope>
SOAP 1.2 では階層的な Code/Subcode 構造が導入され、複数言語の Reason テキストがサポートされました。用語も変更され、Client は Sender に、Server は Receiver になりました。
SOAP 1.1 Fault コードから HTTP ステータスへのマッピング
標準 Fault コード
| SOAP 1.1 faultcode | HTTP ステータス | 理由 |
|---|---|---|
soap:Client | 400 Bad Request | クライアントが不正なデータを送信 |
soap:Server | 502 Bad Gateway | 上流 SOAP サーバーの処理失敗 |
soap:VersionMismatch | 400 Bad Request | クライアントが誤った SOAP バージョンを使用 |
soap:MustUnderstand | 501 Not Implemented | サーバーが必須ヘッダーを処理できない |
Client Fault の詳細マッピング
soap:Client は広範なカテゴリです。実際には faultstring と detail 要素に含まれる手がかりから、より具体的な HTTP ステータスへのマッピングが可能です:
| faultstring のパターン | HTTP ステータス | 使用場面 |
|---|---|---|
| "Authentication failed" / "Invalid credentials" | 401 Unauthorized | 認証情報の欠落または不正 |
| "Access denied" / "Not authorized" | 403 Forbidden | 認証は有効だが権限不足 |
| "Resource not found" / "No such account" | 404 Not Found | 参照先のエンティティが存在しない |
| "Duplicate request" / "Already exists" | 409 Conflict | リソースの状態の競合 |
| "Input validation failed" / "Invalid format" | 400 Bad Request | 入力データの形式不正 |
| "Rate limit exceeded" / "Too many requests" | 429 Too Many Requests | スロットリング適用 |
| "Request too large" / "Maximum size exceeded" | 413 Payload Too Large | 入力がサイズ制限を超過 |
Server Fault の詳細マッピング
| faultstring のパターン | HTTP ステータス | 使用場面 |
|---|---|---|
| "Internal error" / "Unexpected exception" | 502 Bad Gateway | 上流の汎用エラー |
| "Service unavailable" / "System maintenance" | 503 Service Unavailable | 上流の一時的な停止 |
| "Timeout" / "Downstream timeout" | 504 Gateway Timeout | 上流の応答遅延 |
| "Database error" / "Connection pool exhausted" | 502 Bad Gateway | 上流インフラの障害 |
SOAP 1.2 Fault コードから HTTP ステータスへのマッピング
標準 Fault コード
| SOAP 1.2 Code | HTTP ステータス | 理由 |
|---|---|---|
soap:Sender | 400 Bad Request | クライアント側のエラー |
soap:Receiver | 502 Bad Gateway | サーバー側のエラー |
soap:VersionMismatch | 400 Bad Request | エンベロープの SOAP バージョンが不正 |
soap:MustUnderstand | 501 Not Implemented | 処理不可能な必須ヘッダー |
soap:DataEncodingUnknown | 400 Bad Request | サポートされていないエンコーディング |
Subcode のマッピング
SOAP 1.2 の Subcode はより細かいエラー分類を提供します:
| Subcode (一般的なパターン) | HTTP ステータス | 説明 |
|---|---|---|
tns:AuthenticationRequired | 401 Unauthorized | 認証情報の欠落 |
tns:AuthenticationFailed | 401 Unauthorized | 認証情報が不正 |
tns:AuthorizationFailed | 403 Forbidden | 権限不足 |
tns:ResourceNotFound | 404 Not Found | エンティティが存在しない |
tns:ValidationFailed | 422 Unprocessable Entity | 意味的に不正な入力 |
tns:ConcurrencyConflict | 409 Conflict | 楽観ロックの失敗 |
tns:QuotaExceeded | 429 Too Many Requests | 使用量の上限到達 |
tns:ServiceUnavailable | 503 Service Unavailable | 一時的な停止 |
統合マッピング表
SOAP 両バージョンの統合リファレンス表です:
| SOAP 1.1 | SOAP 1.2 | HTTP | カテゴリ |
|---|---|---|---|
soap:Client | soap:Sender | 400 | 汎用クライアントエラー |
soap:Client + 認証失敗 | soap:Sender + AuthenticationFailed | 401 | 認証 |
soap:Client + アクセス拒否 | soap:Sender + AuthorizationFailed | 403 | 認可 |
soap:Client + 見つからない | soap:Sender + ResourceNotFound | 404 | リソース不在 |
soap:Client + 競合 | soap:Sender + ConcurrencyConflict | 409 | 状態の競合 |
soap:Client + バリデーション | soap:Sender + ValidationFailed | 422 | 意味的エラー |
soap:Client + レート制限 | soap:Sender + QuotaExceeded | 429 | スロットリング |
soap:Server | soap:Receiver | 502 | 汎用サーバーエラー |
soap:Server + 利用不可 | soap:Receiver + ServiceUnavailable | 503 | 一時停止 |
soap:Server + タイムアウト | soap:Receiver + Timeout | 504 | 上流タイムアウト |
soap:VersionMismatch | soap:VersionMismatch | 400 | プロトコルエラー |
soap:MustUnderstand | soap:MustUnderstand | 501 | 未サポート機能 |
SOAP Fault Detail からのエラー情報抽出
detail 要素は最も有用なデバッグ情報が含まれる場所ですが、その構造は完全にサービス固有です。プログラム的に抽出する方法を示します。
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 と 1.2 の両方に対応
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;
}
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)
# SOAP 1.1 の名前空間を先に試し、次に 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
# Fault コードとメッセージを抽出 (1.1 と 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"
)
# 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
REST エラーレスポンスの設計
SOAP Fault をパースして HTTP ステータスにマッピングしたら、一貫したフォーマットで REST エラーレスポンスを構築します:
{
"error": {
"type": "soap_fault",
"code": "AUTHENTICATION_FAILED",
"message": "上流 SOAP サービスの認証に失敗しました",
"detail": {
"soap_fault_code": "soap:Client",
"soap_fault_string": "Authentication failed: invalid username or password",
"upstream_service": "PaymentService"
}
}
}
REST エラーレスポンス設計のガイドライン:
- 元の SOAP Fault コードを detail に含める — 下流の開発者が上流サービスプロバイダーとのデバッグに必要になる場合がある
- 機密情報をサニタイズする — SOAP Fault の detail 要素から内部サーバーパス、スタックトレース、データベース情報を漏洩させない
- 一貫したエラーコードを使用する — SOAP Fault を独自のエラーコード体系にマッピング (例:
AUTHENTICATION_FAILED、VALIDATION_ERROR、UPSTREAM_UNAVAILABLE) - 上流サービス名を含める — 複数の SOAP サービスをプロキシする場合、どのサービスがエラーを発生させたか識別する
カスタム Fault の処理
多くの SOAP サービスは WSDL 内で独自の Fault 型を定義しています。これらは detail 要素に出現し、サービス固有のパースが必要です。
例: 独自のエラー構造を定義する決済サービス:
<soap:Fault>
<faultcode>soap:Client</faultcode>
<faultstring>決済処理に失敗しました</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>JPY</pay:currency>
</pay:PaymentError>
</detail>
</soap:Fault>
この Fault に対する適切な REST レスポンス:
{
"error": {
"type": "payment_error",
"code": "INSUFFICIENT_FUNDS",
"message": "決済処理に失敗しました: 残高不足",
"detail": {
"available_balance": 45.00,
"requested_amount": 100.00,
"currency": "JPY"
}
}
}
HTTP ステータスは 422 Unprocessable Entity (リクエストは有効だがビジネスロジックが拒否) が適切です。400 (リクエストの形式不正を意味する) ではありません。
エッジケースと注意点
1. HTTP 200 で SOAP Fault が返るケース
多くの SOAP サービスはレスポンスに SOAP Fault が含まれていても HTTP 200 を返します。プロキシは HTTP ステータスだけでなく、常にレスポンスボディをパースして Fault を検出する必要があります:
// 誤り — HTTP 200 で返された Fault を見逃す
if (response.status === 200) {
return parseSuccessResponse(response.data);
}
// 正しい — 常にボディの Fault をチェック
const fault = parseSoapFault(response.data);
if (fault) {
return buildErrorResponse(fault);
}
return parseSuccessResponse(response.data);
2. SOAP 1.2 の HTTP ステータスコード規約
SOAP 1.2 の仕様では、Sender Fault は HTTP 400、Receiver Fault は HTTP 500 を返すべきとされています。しかし、すべての実装がこれに従っているわけではありません。常に XML ボディをパースしてください。
3. 空の Detail 要素
一部のサービスは空の detail 要素やホワイトスペースのみの要素を返すことがあります。パース失敗ではなく「detail なし」として扱います。
4. 複数の Fault Subcode
SOAP 1.2 ではネストされた Subcode が許可されています。チェーンを辿って最も具体的なコードを見つけます:
def get_deepest_subcode(code_element, ns):
"""ネストされた Subcode を辿り最も具体的なコードを取得"""
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
SOAPless による解決
正確な SOAP Fault から HTTP ステータスへのマッピングの実装は、手間がかかりミスが起きやすい作業です。SOAP サービスごとに異なる Fault パターン、カスタムの detail 構造、HTTP ステータスコードにまつわるエッジケースがあります。
SOAPless はこの変換を自動的に処理します。上流の SOAP サービスが Fault を返すと、SOAPless が Fault コードをパースし、faultstring と detail 要素を分析し、適切にマッピングされた HTTP ステータスコードとクリーンな JSON エラーレスポンスを返します。マッピングロジックは SOAP 1.1 と 1.2 の両方をカバーし、HTTP 200 で Fault が返るエッジケースにも対応し、カスタム detail 要素から構造化された情報を抽出します。Fault パースコードの記述やメンテナンスなしに、REST コンシューマーが一貫した整形済みのエラーレスポンスを受け取れます。