2025, Dec 25 12:01
Как починить suds при неожиданных полях SOAP-ответа
Как избежать ошибки Type not found: accountNumber в suds при расхождении SOAP-ответа с WSDL: причина и MessagePlugin, который отсекает неизвестные поля.
Когда SOAP-бэкенд начинает возвращать поля, которых нет в WSDL, suds может «задавиться» на этапе разборки (unmarshalling). В suds v1.1.1 ответ с неожиданным элементом приводит к ошибке «Type not found: ‘accountNumber’», даже если отключены faults и включены флаги вроде allowUnknownMessageParts. Ниже — воспроизводимая форма проблемы и практичный способ сделать клиента устойчивым без изменений на стороне сервиса.
Минимальная конфигурация, которая всё равно падает
Инициализация выглядит простой, но при этом библиотека по‑прежнему срывается на неизвестных элементах в теле ответа:
from suds.client import Client
wsdl_uri = "path_or_url_to_wsdl"
soap_client = Client(wsdl_uri, faults=False)
soap_client.set_options(allowUnknownMessageParts=True, extraArgumentErrors=False)
Сервер отвечает валидным SOAP‑конвертом, в котором способ оплаты содержит поле, отсутствующее в WSDL. Библиотека сообщает об ошибке:
Type not found: ‘accountNumber’
Соответствующий фрагмент XML выглядит так:
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<ns2:getTransactionResponse xmlns:ns2="http://soap.api.controller.web.payjar.com/">
<return>
<basket>
<amountInCents>500</amountInCents>
<currencyCode>ZAR</currencyCode>
<description>ABC Retailer</description>
</basket>
<displayMessage>Successful</displayMessage>
<merchantReference>114ff7d7-3a6d-44e1-bff7-dfjkheuiier789</merchantReference>
<payUReference>dfskjskjdfhjkksk</payUReference>
<paymentMethodsUsed xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="ns2:capitecPay">
<accountNumber>12347383643</accountNumber>
<amountInCents>500</amountInCents>
<beneficiaryStatementDescription>CAPITEC1748237482597</beneficiaryStatementDescription>
<transactionId>a8f64064-39f2-11f0-9fde-ejnsduinsiuefnf</transactionId>
</paymentMethodsUsed>
<resultCode>00</resultCode>
<resultMessage>Successful</resultMessage>
<successful>true</successful>
<transactionState>SUCCESSFUL</transactionState>
<transactionType>PAYMENT</transactionType>
</return>
</ns2:getTransactionResponse>
<soap:Body>
</soap:Envelope>
В чём реальная причина
В ответе указана разновидность способа оплаты типа capitecPay, внутри которой присутствует элемент accountNumber. Этого поля нет в схеме WSDL. Во время unmarshalling suds сопоставляет узлы ответа со схемой, натыкается на неизвестный элемент и выбрасывает исключение «Type not found». Обычные переключатели вроде faults=False и allowUnknownMessageParts=True от этой несостыковки не спасают.
Точечный обходной путь с помощью MessagePlugin в suds
Самый надёжный способ удержать клиента «на плаву» — перехватить этап разборки и нормализовать ответ до того, как suds начнёт его валидировать. Приведённый ниже плагин отсекает неизвестных потомков согласно модели схемы, чтобы клиент видел только те узлы, которые ему знакомы.
from suds.client import Client
from suds.plugin import MessagePlugin
class StripUnknownPartsHook(MessagePlugin):
def unmarshalled(self, ctx):
if hasattr(ctx.reply, 'children'):
valid = [node for node in ctx.reply.children
if node.name in ctx.reply.__metadata__.sxtype.rawchildren]
ctx.reply.children = valid
wsdl_uri = "path_or_url_to_wsdl"
soap_client = Client(wsdl_uri, plugins=[StripUnknownPartsHook()], faults=False)
В рассматриваемом случае с paymentMethodsUsed, где сервер подмешивает тип и поля, не объявленные в WSDL, полезно гарантировать, что эти элементы не провалят валидацию. Следующий плагин сохраняет поведение из примера выше и одновременно допускает незнакомые элементы внутри paymentMethodsUsed, при необходимости подставляя им совместимые метаданные.
from suds.client import Client
from suds.plugin import MessagePlugin
class PayUPayloadNormalizerHook(MessagePlugin):
def unmarshalled(self, ctx):
if hasattr(ctx.reply, 'paymentMethodsUsed'):
for pm in ctx.reply.paymentMethodsUsed:
if not hasattr(pm, '__metadata__'):
pm.__metadata__ = ctx.reply.__metadata__
if hasattr(ctx.reply, 'children'):
keep = [n for n in ctx.reply.children
if n.name in ctx.reply.__metadata__.sxtype.rawchildren]
ctx.reply.children = keep
return ctx.reply
wsdl_uri = "path_or_url_to_wsdl"
soap_client = Client(
wsdl_uri,
plugins=[PayUPayloadNormalizerHook()],
faults=False,
allowUnknownMessageParts=True
)
Зачем это важно
Расхождение между WSDL и реальными ответами может проявиться в любой момент. Если клиент строго соблюдает контракт, даже безобидное дополнительное поле способно обрушить вызов. Санитизация или нормализация входящего payload на границе unmarshalling позволяет сохранять стабильность интеграции, пока апстрим эволюционирует. Когда WSDL обновят и включат новые поля, обходной механизм станет не нужен.
Практические замечания и выводы
Если вы инициализируете клиента заранее, а позже снова вызываете set_options, добавляя заголовки или иные настройки, можно ненароком перезаписать прежние флаги или потерять конфигурацию плагинов. Проверьте, что итоговый экземпляр клиента сохраняет allowUnknownMessageParts и нужный вам плагин.
Когда сервер добавляет новый элемент вроде accountNumber под типом capitecPay, которого нет в WSDL, подход с плагином позволяет игнорировать неизвестное поле и продолжать работу с теми, которые ваш клиент ожидает.
В долгосрочной перспективе следите за WSDL и сверяйте его с ответами сервера. Пока этого не произошло, узконаправленный MessagePlugin остаётся надёжной страховкой.