pythonsoap-errorstroubleshooting

Python Zeep の SOAP エラー: Fault、TransportError、ValidationError のトラブルシューティング

SOAPless Team10 min read

Python で SOAP サービスを呼び出すライブラリとして最も広く使われている Zeep ですが、エラーが発生するとメッセージが分かりにくく、原因の特定に時間がかかりがちです。この記事では、Zeep で頻出するエラーをすべて取り上げ、原因の切り分けと具体的な修正コードを紹介します。

エラー 1: zeep.exceptions.Fault

Fault は SOAP サーバーがリクエストを処理した結果、エラーを返したことを示す例外です。HTTP レベルでは 200 OK が返っていても、レスポンスの XML 内に Fault 要素が含まれています。

from zeep import Client
from zeep.exceptions import Fault
from lxml import etree

client = Client('https://example.com/service?wsdl')

try:
    result = client.service.GetUser(user_id=999)
except Fault as e:
    print(f"Fault code: {e.code}")
    print(f"Fault message: {e.message}")
    # detail 要素がある場合は XML として出力
    if e.detail is not None:
        print(etree.tostring(e.detail, pretty_print=True).decode())

サーバーが返す SOAP Fault XML の例を示します。

<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <soap:Body>
    <soap:Fault>
      <faultcode>soap:Server</faultcode>
      <faultstring>User not found</faultstring>
      <detail>
        <ErrorCode>USR_404</ErrorCode>
        <ErrorMessage>No user exists with ID 999</ErrorMessage>
      </detail>
    </soap:Fault>
  </soap:Body>
</soap:Envelope>

主な原因と対処法:

  • soap:Client: リクエストの構造やデータ型が不正。WSDL のスキーマ定義と照合して、必須フィールドや型を確認します。
  • soap:Server: サーバー側の処理でエラーが発生。detail 要素のエラーコードをサービス提供者のドキュメントで確認します。
  • 認証エラー: WS-Security ヘッダーや HTTP 認証情報が不正。

エラー 2: zeep.exceptions.TransportError

TransportError は HTTP レベルで通信が失敗した場合に発生します。SOAP レスポンスを受信できなかったことを意味します。

zeep.exceptions.TransportError: Server returned HTTP status 503 (b'Service Unavailable')

ステータスコード別の対処:

ステータス原因
401 / 403認証情報が未設定または不正
404エンドポイント URL が間違っている
500サーバー側の障害 (レスポンスボディに Fault がある場合も)
503サービスの一時停止、メンテナンス中
ConnectionErrorネットワーク障害、DNS 解決失敗、ファイアウォール

修正: タイムアウトとリトライの設定

from requests import Session
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
from zeep.transports import Transport

session = Session()
retry = Retry(
    total=3,
    backoff_factor=0.5,
    status_forcelist=[500, 502, 503, 504],
)
adapter = HTTPAdapter(max_retries=retry)
session.mount('http://', adapter)
session.mount('https://', adapter)

transport = Transport(session=session, timeout=30)
client = Client('https://example.com/service?wsdl', transport=transport)

修正: SSL 証明書の問題

開発環境や社内サービスで自己署名証明書を使っている場合は、カスタム CA バンドルを指定します。

session = Session()
# カスタム CA 証明書を指定
session.verify = '/path/to/custom-ca-bundle.crt'
# 検証を無効化 (本番環境では絶対に使わないこと)
# session.verify = False

transport = Transport(session=session)
client = Client('https://example.com/service?wsdl', transport=transport)

エラー 3: zeep.exceptions.ValidationError

Zeep はリクエスト送信前に WSDL スキーマに基づいてバリデーションを実行します。ValidationError はデータが期待される型や構造に合っていない場合に発生します。

zeep.exceptions.ValidationError: Missing element AccountNumber (CreateInvoice.AccountNumber)

よくあるケース:

  1. 必須フィールドの欠落: WSDL で minOccurs="1" と定義されたフィールドが未指定。
  2. データ型の不一致: 数値型のフィールドに文字列を渡している。
  3. 複合型の構造が不正: ネストされたオブジェクトを Zeep の型ファクトリで正しく構築していない。

修正: 型ファクトリを使った複合型の構築

client = Client('https://example.com/billing?wsdl')

# オペレーションが期待する型を確認
print(client.service.CreateInvoice.__doc__)

# 複合型を型ファクトリで構築
InvoiceType = client.get_type('ns0:Invoice')
LineItemType = client.get_type('ns0:LineItem')

invoice = InvoiceType(
    AccountNumber='ACCT-001',
    InvoiceDate='2025-11-19',
    LineItems={
        'LineItem': [
            LineItemType(ProductCode='WIDGET-A', Quantity=10, UnitPrice=29.99),
            LineItemType(ProductCode='WIDGET-B', Quantity=5, UnitPrice=49.99),
        ]
    }
)

result = client.service.CreateInvoice(invoice)

修正: strict モードの無効化

WSDL のスキーマ定義が実際のサービスの挙動と異なる場合、strict モードを無効にすることで回避できます。

from zeep import Settings

settings = Settings(strict=False, xml_huge_tree=True)
client = Client('https://example.com/service?wsdl', settings=settings)

strict=False を設定すると、Zeep は型の不一致を許容します。WSDL の定義が不正確だと分かっている場合に使用してください。

エラー 4: lxml.etree.XMLSyntaxError

このエラーは、Zeep が受信したデータが有効な XML でない場合に発生します。WSDL の読み込み時やレスポンスのパース時に見られます。

lxml.etree.XMLSyntaxError: Opening and ending tag mismatch: hr line 3 and body, line 4, column 8

主な原因:

  • URL が XML ではなく HTML ページ (ログインページ、エラーページ) を返している
  • プロキシやロードバランサーがレスポンスを途中で切断している
  • エンコーディングの不一致 (XML 宣言は UTF-8 だが、実際のバイト列が異なる)

修正: HistoryPlugin で生のレスポンスを確認

from zeep.plugins import HistoryPlugin
from lxml import etree

history = HistoryPlugin()
client = Client('https://example.com/service?wsdl', plugins=[history])

try:
    result = client.service.GetUser(user_id=1)
except Exception:
    # サーバーが返した生の XML を確認
    if history.last_received and 'envelope' in history.last_received:
        print(etree.tostring(
            history.last_received['envelope'], pretty_print=True
        ).decode())
    else:
        # XML ではないレスポンスが返ってきた場合
        print("レスポンスが XML ではありません。直接 HTTP リクエストで確認してください。")

XML ではないレスポンスが返ってきている場合は、requests で直接確認します。

import requests

response = requests.post(
    'https://example.com/service',
    data='<test/>',
    headers={'Content-Type': 'text/xml'}
)
print(f"Status: {response.status_code}")
print(f"Content-Type: {response.headers.get('Content-Type')}")
print(f"Body: {response.text[:500]}")

エラー 5: WSDL の読み込み失敗

Client() のインスタンス化時に失敗する場合、WSDL 自体に問題があります。

requests.exceptions.ConnectionError: HTTPSConnectionPool(host='example.com', port=443): Max retries exceeded

修正: WSDL をローカルファイルから読み込む

WSDL の URL が不安定な場合や VPN 経由でしかアクセスできない場合は、事前にダウンロードしてローカルから読み込みます。

# WSDL をダウンロード
import requests
resp = requests.get('https://example.com/service?wsdl')
with open('service.wsdl', 'w') as f:
    f.write(resp.text)

# ローカルファイルから読み込み
client = Client('service.wsdl')

WSDL が他の XSD ファイルをインポートしている場合は、認証付きの Transport を設定します。

from zeep.transports import Transport
from requests import Session

session = Session()
session.auth = ('user', 'password')
transport = Transport(session=session)

# Zeep は wsdl:import や xsd:import を自動で追跡します
client = Client('https://example.com/service?wsdl', transport=transport)

エラー 6: 名前空間の不一致

名前空間の問題は、オペレーションが無応答になったり空のレスポンスが返ったりする原因として特に厄介です。サーバーが要素を認識できないため、エラーにもならず沈黙します。

# Zeep が WSDL から検出した名前空間を確認
for service in client.wsdl.services.values():
    print(f"Service: {service.name}")
    for port_name, port in service.ports.items():
        print(f"  Port: {port_name}")
        for operation in port.binding._operations.values():
            print(f"    Operation: {operation.name}")

デバッグのベストプラクティス: ログ出力

Zeep のデバッグで最も有効な手法は、送受信される生の XML をすべてログに出力することです。

import logging
from zeep.plugins import HistoryPlugin
from lxml import etree

# Zeep の組み込みログを有効化
logging.getLogger('zeep.transports').setLevel(logging.DEBUG)

# または HistoryPlugin でプログラム的にアクセス
history = HistoryPlugin()
client = Client('https://example.com/service?wsdl', plugins=[history])

try:
    result = client.service.GetUser(user_id=1)
finally:
    for hist in [history.last_sent, history.last_received]:
        if hist and 'envelope' in hist:
            print(etree.tostring(hist['envelope'], pretty_print=True).decode())

送信した XML と受信した XML を比較することで、ほとんどの問題は特定できます。

SOAPless による解決

Zeep のエラーをデバッグするには、WSDL パース、名前空間解決、XML シリアライズ、トランスポート設定のすべてを同時に理解する必要があります。プロダクション環境で安定した連携を構築するなら、SOAPless はまったく異なるアプローチを提供します。

WSDL URL を SOAPless に貼り付けるだけで、REST JSON エンドポイントが自動生成されます。Python のコードは単純な requests.post() に JSON ボディを渡すだけになり、XML の構築も名前空間の解決も lxml への依存も不要です。SOAPless がサーバー側で SOAP エンベロープの構築、名前空間解決、XML-JSON 変換を処理するため、TransportErrorValidationErrorXMLSyntaxError はアプリケーションコードで対処すべき問題ではなくなります。

ダッシュボードでオペレーションを事前にテストでき、OpenAPI 仕様も自動生成されるため、SOAP 固有のツールではなく標準的な API ツールで開発を進められます。