From 562f9b1194cef1f31a71cf5b79b137c4bed853dc Mon Sep 17 00:00:00 2001 From: Dylan Merigaud Date: Sat, 3 Oct 2026 17:02:04 +0200 Subject: [PATCH] Ship the SOAP encoding schema instead of fetching it zeep loads http://schemas.xmlsoap.org/soap/encoding/ whenever a WSDL uses SOAP-ENC types, including through the automatic import of that namespace. When the server is unreachable or answers 404 the WSDL fails to load, and with forbid_external enabled the load is refused. Ship a copy of the schema (the file already used by the tests, with its W3C license notice unchanged) and load it from the package for the http and https URLs instead of going through the transport. When the file is missing, for example in an application packaged without zeep's package data, the schema is loaded through the transport as before. Fixes: https://github.com/mvantellingen/python-zeep/issues/1417 Co-Authored-By: Claude Opus 5.5 --- src/zeep/loader.py | 37 ++ src/zeep/schemas/soap-encoding.xsd | 535 +++++++++++++++++++++++++++++ tests/test_loader.py | 26 +- tests/test_wsdl_arrays.py | 31 ++ 4 files changed, 628 insertions(+), 1 deletion(-) create mode 100644 src/zeep/schemas/soap-encoding.xsd diff --git a/src/zeep/loader.py b/src/zeep/loader.py index 5fd3fdbd5..0d394d16a 100644 --- a/src/zeep/loader.py +++ b/src/zeep/loader.py @@ -1,3 +1,4 @@ +import importlib.resources import os.path import typing from urllib.parse import urljoin, urlparse, urlunparse @@ -13,6 +14,36 @@ ) from zeep.settings import Settings +# Well known schemas which are shipped with zeep, so loading them doesn't +# depend on the availability of the remote server (see #1417). The SOAP +# encoding schema is also auto imported by zeep when a WSDL references it +# without importing it, see ``zeep.xsd.const.AUTO_IMPORT_NAMESPACES``. +BUNDLED_SCHEMAS = { + "http://schemas.xmlsoap.org/soap/encoding/": "soap-encoding.xsd", + "https://schemas.xmlsoap.org/soap/encoding/": "soap-encoding.xsd", +} + + +def load_bundled_schema(url) -> typing.Optional[bytes]: + """Return the content of the schema shipped with zeep for the given url, + or None if zeep doesn't ship one or the file is missing. + + """ + filename = BUNDLED_SCHEMAS.get(str(url)) + if filename is None: + return None + try: + return ( + importlib.resources.files("zeep") + .joinpath("schemas") + .joinpath(filename) + .read_bytes() + ) + except OSError: + # The file is missing when zeep is packaged without its package data + # (for example by PyInstaller), load it through the transport instead. + return None + class ImportResolver(Resolver): """Custom lxml resolve to use the transport object""" @@ -102,6 +133,9 @@ def load_external( else: if base_url: url = absolute_location(url, base_url) + bundled = load_bundled_schema(url) + if bundled is not None: + return parse_xml(bundled, transport, base_url, settings=settings) if not _initial and settings.forbid_external: if urlparse(str(url)).scheme in ("http", "https"): raise ExternalReferenceForbidden(url) @@ -135,6 +169,9 @@ async def load_external_async( else: if base_url: url = absolute_location(url, base_url) + bundled = load_bundled_schema(url) + if bundled is not None: + return parse_xml(bundled, transport, base_url, settings=settings) if not _initial and settings.forbid_external: if urlparse(str(url)).scheme in ("http", "https"): raise ExternalReferenceForbidden(url) diff --git a/src/zeep/schemas/soap-encoding.xsd b/src/zeep/schemas/soap-encoding.xsd new file mode 100644 index 000000000..de4a44aab --- /dev/null +++ b/src/zeep/schemas/soap-encoding.xsd @@ -0,0 +1,535 @@ + + + + + + + + + 'root' can be used to distinguish serialization roots from other + elements that are present in a serialization but are not roots of + a serialized value graph + + + + + + + + + + + + + Attributes common to all elements that function as accessors or + represent independent (multi-ref) values. The href attribute is + intended to be used in a manner like CONREF. That is, the element + content should be empty iff the href attribute appears + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 'Array' is a complex type for accessors identified by position + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/test_loader.py b/tests/test_loader.py index 33ff3f814..b13146d02 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -2,7 +2,7 @@ from tests.utils import DummyTransport from zeep.exceptions import DTDForbidden, EntitiesForbidden, ExternalReferenceForbidden -from zeep.loader import load_external, parse_xml +from zeep.loader import BUNDLED_SCHEMAS, load_external, parse_xml from zeep.settings import Settings @@ -79,3 +79,27 @@ def test_forbid_external_default_allows_load(): tree = load_external("http://example.com/a.xsd", transport) assert tree.tag == "root" + + +@pytest.mark.parametrize( + "url", + [ + "http://schemas.xmlsoap.org/soap/encoding/", + "https://schemas.xmlsoap.org/soap/encoding/", + ], +) +def test_load_external_uses_bundled_soap_encoding_schema(url): + # Nothing is bound on the transport and remote loads are forbidden, so + # only the copy shipped with zeep can be used (#1417) + settings = Settings(forbid_external=True) + tree = load_external(url, DummyTransport(), settings=settings) + assert tree.get("targetNamespace") == "http://schemas.xmlsoap.org/soap/encoding/" + + +def test_missing_bundled_schema_falls_back_to_transport(monkeypatch): + # Packagers like PyInstaller can leave out the package data + url = "http://schemas.xmlsoap.org/soap/encoding/" + monkeypatch.setitem(BUNDLED_SCHEMAS, url, "missing.xsd") + transport = DummyTransport() + transport.bind(url, b"") + assert load_external(url, transport).tag == "root" diff --git a/tests/test_wsdl_arrays.py b/tests/test_wsdl_arrays.py index 47e61a129..98a69b90c 100644 --- a/tests/test_wsdl_arrays.py +++ b/tests/test_wsdl_arrays.py @@ -608,3 +608,34 @@ def test_xml_soap_enc_string(transport): obj = shoe_type.parse(node[0], schema) assert obj[0]["_value_1"] == "foo" + + +def test_soap_array_without_remote_soap_encoding_schema(): + # The SOAP encoding schema ships with zeep, so resolving SOAP-ENC:Array + # works when schemas.xmlsoap.org cannot be reached (#1417). + schema = xsd.Schema( + load_xml( + """ + + + + + + + + + + + """ + ), + transport=DummyTransport(), + ) + + ArrayOfString = schema.get_type("ns0:ArrayOfString") + node = render_node(ArrayOfString, ArrayOfString(["item", "and"])) + data = ArrayOfString.parse_xmlelement(node, schema) + assert data == ["item", "and"]