Python's Zeep library is the most popular way to consume SOAP services in Python. It handles WSDL parsing, XML serialization, and transport — but when things go wrong, the error messages can be cryptic. This guide covers every common Zeep error, explains what causes it, and gives you working code to fix it.
Error 1: zeep.exceptions.Fault
A Fault exception means the SOAP server processed your request but returned a SOAP Fault element. This is the SOAP equivalent of an HTTP error response.
from zeep import Client
from zeep.exceptions import Fault
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}")
print(f"Fault detail: {e.detail}")
The raw SOAP Fault XML looks like this:
<?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>
Common causes and fixes:
soap:Clientfault code: Your request is malformed. Check required fields, data types, and enum values against the WSDL.soap:Serverfault code: The server-side service failed. Check thedetailelement for specific error codes.- Authentication faults: The service rejected your credentials. Verify your WS-Security headers or HTTP auth.
# Inspect the full fault detail as an lxml element
from lxml import etree
try:
result = client.service.GetUser(user_id=999)
except Fault as e:
if e.detail is not None:
print(etree.tostring(e.detail, pretty_print=True).decode())
Error 2: zeep.exceptions.TransportError
A TransportError means Zeep failed at the HTTP level — it never got a valid SOAP response. This usually wraps an HTTP status code.
zeep.exceptions.TransportError: Server returned HTTP status 503 (b'Service Unavailable')
Diagnosing with status codes:
| HTTP Status | Likely Cause |
|---|---|
| 401 / 403 | Missing or invalid authentication |
| 404 | Wrong endpoint URL |
| 500 | Server-side crash (check Fault in body) |
| 503 | Service temporarily unavailable |
| 0 / ConnectionError | Network issue, DNS failure, firewall |
Fix: Configure timeouts and retries
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)
Fix: Handle SSL certificate issues
session = Session()
# For self-signed certificates in development only
session.verify = '/path/to/custom-ca-bundle.crt'
# Or disable verification (NEVER in production)
# session.verify = False
transport = Transport(session=session)
client = Client('https://example.com/service?wsdl', transport=transport)
Error 3: zeep.exceptions.ValidationError
Zeep validates your input against the WSDL schema before sending the request. A ValidationError means your data doesn't match the expected types or structure.
zeep.exceptions.ValidationError: Missing element AccountNumber (CreateInvoice.AccountNumber)
Common scenarios:
- Missing required fields: The WSDL defines
minOccurs="1"but you didn't supply the value. - Wrong data types: Passing a string where an integer is expected.
- Incorrect complex type structure: Nested objects need to be created with Zeep's type factory.
Fix: Use the type factory for complex types
client = Client('https://example.com/billing?wsdl')
# Inspect what the operation expects
print(client.service.CreateInvoice.__doc__)
# Build complex types properly
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)
Fix: Disable strict validation when the WSDL is inaccurate
Some WSDL files have incorrect schema definitions. Zeep's strict mode (enabled by default) will reject valid data if the WSDL is wrong.
from zeep import Settings
settings = Settings(strict=False, xml_huge_tree=True)
client = Client('https://example.com/service?wsdl', settings=settings)
Setting strict=False tells Zeep to be lenient about type mismatches. Use this when you know the service accepts data that doesn't match its own WSDL.
Error 4: lxml.etree.XMLSyntaxError
This error means Zeep received something that isn't valid XML. It usually happens during WSDL loading or response parsing.
lxml.etree.XMLSyntaxError: Opening and ending tag mismatch: hr line 3 and body, line 4, column 8
Common causes:
- The URL returns an HTML page (login page, error page, proxy page) instead of XML.
- The response is truncated due to a proxy or load balancer cutting the connection.
- Encoding mismatch: The XML declares
UTF-8but the actual bytes are in a different encoding.
Fix: Inspect the raw response
from zeep.plugins import HistoryPlugin
history = HistoryPlugin()
client = Client('https://example.com/service?wsdl', plugins=[history])
try:
result = client.service.GetUser(user_id=1)
except Exception:
# See what the server actually returned
print("Request:")
print(etree.tostring(history.last_sent['envelope'], pretty_print=True).decode())
print("\nResponse:")
print(etree.tostring(history.last_received['envelope'], pretty_print=True).decode())
If history.last_received is None, the response wasn't XML at all. Use the raw session to see what came back:
session = Session()
transport = Transport(session=session)
client = Client('https://example.com/service?wsdl', transport=transport)
# After a failed call, check the response directly
response = session.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]}")
Error 5: WSDL Loading Failures
When Client() fails during initialization, the WSDL itself is the problem.
requests.exceptions.ConnectionError: HTTPSConnectionPool(host='example.com', port=443): Max retries exceeded
Fix: Load WSDL from a local file
If the WSDL URL is unreliable or behind a VPN, download it once and load it locally:
# Download the WSDL
import requests
resp = requests.get('https://example.com/service?wsdl')
with open('service.wsdl', 'w') as f:
f.write(resp.text)
# Load from local file
client = Client('service.wsdl')
For WSDLs that import other schemas (XSD files), you need to handle the imports:
from zeep.transports import Transport
from requests import Session
session = Session()
session.auth = ('user', 'password') # If WSDL requires auth
transport = Transport(session=session)
# Zeep will follow wsdl:import and xsd:import references
client = Client('https://example.com/service?wsdl', transport=transport)
Error 6: Namespace Mismatches
Namespace issues cause operations to silently fail or return empty responses. The server doesn't recognize the elements because they're in the wrong XML namespace.
# Check what namespaces Zeep found in the WSDL
from zeep.helpers import serialize_object
# List all available services and ports
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}")
Fix: Manually set the SOAPAction or target namespace
from zeep import Client
client = Client('https://example.com/service?wsdl')
# Override the SOAPAction header if the WSDL has it wrong
with client.settings(force_https=False):
result = client.service.GetUser(
user_id=1,
_soapheaders={'SOAPAction': 'http://example.com/GetUser'}
)
Debugging Best Practice: Logging Plugin
The single most useful debugging technique with Zeep is logging all raw XML traffic:
import logging
from zeep.plugins import HistoryPlugin
# Enable Zeep's built-in logging
logging.getLogger('zeep.transports').setLevel(logging.DEBUG)
# Or use the history plugin for programmatic access
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())
This shows you exactly what XML Zeep sends and what the server returns — which is almost always enough to identify the problem.
How SOAPless Helps
Debugging Zeep errors means dealing with WSDL parsing, namespace resolution, XML serialization, and transport configuration — all at once. If you're building an integration that needs to be reliable in production, SOAPless takes a different approach entirely.
Instead of wrestling with Zeep's XML layer, you paste your WSDL URL into SOAPless and get REST JSON endpoints immediately. Your Python code becomes a simple requests.post() call with a JSON body — no XML, no namespace issues, no lxml dependencies. SOAPless handles the SOAP envelope construction, namespace resolution, and XML-to-JSON conversion on the server side, so TransportError, ValidationError, and XMLSyntaxError are problems you never encounter in your application code.
SOAPless also provides a dashboard where you can test operations before writing any code, and it auto-generates OpenAPI specs so you can use standard API tools instead of SOAP-specific ones.