Skip to content

Commit 43bd958

Browse files
jacalataclaude
andcommitted
fix: handle missing error element in server error responses
Fixes #1083 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 387d043 commit 43bd958

2 files changed

Lines changed: 84 additions & 7 deletions

File tree

‎tableauserverclient/server/endpoint/exceptions.py‎

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,22 @@ def from_response(cls, resp, ns, url):
2929
# Check elements exist before .text
3030
parsed_response = fromstring(resp)
3131
try:
32-
error_response = cls(
33-
parsed_response.find("t:error", namespaces=ns).get("code", ""),
34-
parsed_response.find(".//t:summary", namespaces=ns).text,
35-
parsed_response.find(".//t:detail", namespaces=ns).text,
36-
url,
37-
)
38-
except Exception as e:
32+
error_element = parsed_response.find("t:error", namespaces=ns)
33+
summary_element = parsed_response.find(".//t:summary", namespaces=ns)
34+
detail_element = parsed_response.find(".//t:detail", namespaces=ns)
35+
36+
# Guard against responses that don't contain a t:error element
37+
if error_element is None:
38+
raw = resp.decode("utf-8", errors="replace") if isinstance(resp, bytes) else str(resp)
39+
error_response = cls("", raw, raw, url)
40+
else:
41+
error_response = cls(
42+
error_element.get("code", ""),
43+
summary_element.text if summary_element is not None else "",
44+
detail_element.text if detail_element is not None else "",
45+
url,
46+
)
47+
except Exception:
3948
raise NonXMLResponseError(resp)
4049
return error_response
4150

‎test/test_endpoint.py‎

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@
44

55
import tableauserverclient as TSC
66
from tableauserverclient.server.endpoint import Endpoint
7+
from tableauserverclient.server.endpoint.exceptions import (
8+
FailedSignInError,
9+
NonXMLResponseError,
10+
ServerResponseError,
11+
)
712

813
import requests_mock
914

@@ -91,3 +96,66 @@ def test_set_user_agent_when_blank(server: TSC.Server) -> None:
9196
params = {"headers": {}} # type: ignore
9297
result = Endpoint.set_user_agent(params)
9398
assert result["headers"]["User-Agent"].startswith("Tableau Server Client")
99+
100+
101+
# --- ServerResponseError / FailedSignInError exception parsing (issue #1083) ---
102+
103+
NS = {"t": "http://tableau.com/api"}
104+
105+
STANDARD_ERROR_XML = b"""<?xml version='1.0' encoding='UTF-8'?>
106+
<tsResponse xmlns="http://tableau.com/api">
107+
<error code="401002">
108+
<summary>Unauthorized Access</summary>
109+
<detail>Invalid credentials were provided.</detail>
110+
</error>
111+
</tsResponse>"""
112+
113+
NO_ERROR_ELEMENT_XML = b"""<?xml version='1.0' encoding='UTF-8'?>
114+
<tsResponse xmlns="http://tableau.com/api">
115+
<message>Something went wrong but with no error element</message>
116+
</tsResponse>"""
117+
118+
NOT_XML_CONTENT = b"Internal Server Error (not XML at all)"
119+
120+
121+
def test_server_response_error_standard_xml():
122+
"""Standard XML with a t:error element parses code/summary/detail correctly."""
123+
err = ServerResponseError.from_response(STANDARD_ERROR_XML, NS, "http://test/")
124+
assert err.code == "401002"
125+
assert "Unauthorized" in err.summary
126+
assert "Invalid credentials" in err.detail
127+
128+
129+
def test_server_response_error_no_error_element_does_not_raise():
130+
"""XML without a t:error element must not raise AttributeError (issue #1083)."""
131+
err = ServerResponseError.from_response(NO_ERROR_ELEMENT_XML, NS, "http://test/")
132+
assert err.code == ""
133+
# The raw XML content should appear in summary/detail as the fallback
134+
assert "Something went wrong" in err.summary or len(err.summary) > 0
135+
136+
137+
def test_server_response_error_not_xml_raises_parse_error():
138+
"""Non-XML content causes fromstring to raise a ParseError (not AttributeError)."""
139+
import xml.etree.ElementTree as ET
140+
141+
with pytest.raises(ET.ParseError):
142+
ServerResponseError.from_response(NOT_XML_CONTENT, NS, "http://test/")
143+
144+
145+
def test_failed_sign_in_error_no_error_element_does_not_raise():
146+
"""FailedSignInError shares from_response — same None guard must apply."""
147+
err = FailedSignInError.from_response(NO_ERROR_ELEMENT_XML, NS, "http://test/")
148+
assert err.code == ""
149+
assert isinstance(err, FailedSignInError)
150+
151+
152+
def test_server_response_error_missing_summary_and_detail():
153+
"""XML with t:error but missing summary/detail children falls back gracefully."""
154+
xml = b"""<?xml version='1.0' encoding='UTF-8'?>
155+
<tsResponse xmlns="http://tableau.com/api">
156+
<error code="500001"></error>
157+
</tsResponse>"""
158+
err = ServerResponseError.from_response(xml, NS, "http://test/")
159+
assert err.code == "500001"
160+
assert err.summary == ""
161+
assert err.detail == ""

0 commit comments

Comments
 (0)