soap-errorsrest-apireferenceweb-services

SOAP Fault と HTTP ステータスコードのマッピング: 開発者リファレンスガイド

SOAPless Team14 min read

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 テキストがサポートされました。用語も変更され、ClientSender に、ServerReceiver になりました。

SOAP 1.1 Fault コードから HTTP ステータスへのマッピング

標準 Fault コード

SOAP 1.1 faultcodeHTTP ステータス理由
soap:Client400 Bad Requestクライアントが不正なデータを送信
soap:Server502 Bad Gateway上流 SOAP サーバーの処理失敗
soap:VersionMismatch400 Bad Requestクライアントが誤った SOAP バージョンを使用
soap:MustUnderstand501 Not Implementedサーバーが必須ヘッダーを処理できない

Client Fault の詳細マッピング

soap:Client は広範なカテゴリです。実際には faultstringdetail 要素に含まれる手がかりから、より具体的な 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 CodeHTTP ステータス理由
soap:Sender400 Bad Requestクライアント側のエラー
soap:Receiver502 Bad Gatewayサーバー側のエラー
soap:VersionMismatch400 Bad Requestエンベロープの SOAP バージョンが不正
soap:MustUnderstand501 Not Implemented処理不可能な必須ヘッダー
soap:DataEncodingUnknown400 Bad Requestサポートされていないエンコーディング

Subcode のマッピング

SOAP 1.2 の Subcode はより細かいエラー分類を提供します:

Subcode (一般的なパターン)HTTP ステータス説明
tns:AuthenticationRequired401 Unauthorized認証情報の欠落
tns:AuthenticationFailed401 Unauthorized認証情報が不正
tns:AuthorizationFailed403 Forbidden権限不足
tns:ResourceNotFound404 Not Foundエンティティが存在しない
tns:ValidationFailed422 Unprocessable Entity意味的に不正な入力
tns:ConcurrencyConflict409 Conflict楽観ロックの失敗
tns:QuotaExceeded429 Too Many Requests使用量の上限到達
tns:ServiceUnavailable503 Service Unavailable一時的な停止

統合マッピング表

SOAP 両バージョンの統合リファレンス表です:

SOAP 1.1SOAP 1.2HTTPカテゴリ
soap:Clientsoap:Sender400汎用クライアントエラー
soap:Client + 認証失敗soap:Sender + AuthenticationFailed401認証
soap:Client + アクセス拒否soap:Sender + AuthorizationFailed403認可
soap:Client + 見つからないsoap:Sender + ResourceNotFound404リソース不在
soap:Client + 競合soap:Sender + ConcurrencyConflict409状態の競合
soap:Client + バリデーションsoap:Sender + ValidationFailed422意味的エラー
soap:Client + レート制限soap:Sender + QuotaExceeded429スロットリング
soap:Serversoap:Receiver502汎用サーバーエラー
soap:Server + 利用不可soap:Receiver + ServiceUnavailable503一時停止
soap:Server + タイムアウトsoap:Receiver + Timeout504上流タイムアウト
soap:VersionMismatchsoap:VersionMismatch400プロトコルエラー
soap:MustUnderstandsoap:MustUnderstand501未サポート機能

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 エラーレスポンス設計のガイドライン:

  1. 元の SOAP Fault コードを detail に含める — 下流の開発者が上流サービスプロバイダーとのデバッグに必要になる場合がある
  2. 機密情報をサニタイズする — SOAP Fault の detail 要素から内部サーバーパス、スタックトレース、データベース情報を漏洩させない
  3. 一貫したエラーコードを使用する — SOAP Fault を独自のエラーコード体系にマッピング (例: AUTHENTICATION_FAILEDVALIDATION_ERRORUPSTREAM_UNAVAILABLE)
  4. 上流サービス名を含める — 複数の 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 コンシューマーが一貫した整形済みのエラーレスポンスを受け取れます。