diff --git a/tableauserverclient/server/endpoint/exceptions.py b/tableauserverclient/server/endpoint/exceptions.py
index 9d30ce331..684b03dd6 100644
--- a/tableauserverclient/server/endpoint/exceptions.py
+++ b/tableauserverclient/server/endpoint/exceptions.py
@@ -29,13 +29,22 @@ def from_response(cls, resp, ns, url):
# Check elements exist before .text
parsed_response = fromstring(resp)
try:
- error_response = cls(
- parsed_response.find("t:error", namespaces=ns).get("code", ""),
- parsed_response.find(".//t:summary", namespaces=ns).text,
- parsed_response.find(".//t:detail", namespaces=ns).text,
- url,
- )
- except Exception as e:
+ error_element = parsed_response.find("t:error", namespaces=ns)
+ summary_element = parsed_response.find(".//t:summary", namespaces=ns)
+ detail_element = parsed_response.find(".//t:detail", namespaces=ns)
+
+ # Guard against responses that don't contain a t:error element
+ if error_element is None:
+ raw = resp.decode("utf-8", errors="replace") if isinstance(resp, bytes) else str(resp)
+ error_response = cls("", raw, raw, url)
+ else:
+ error_response = cls(
+ error_element.get("code", ""),
+ summary_element.text if summary_element is not None else "",
+ detail_element.text if detail_element is not None else "",
+ url,
+ )
+ except Exception:
raise NonXMLResponseError(resp)
return error_response
diff --git a/test/test_endpoint.py b/test/test_endpoint.py
index 0b852ab0e..e4670d1fc 100644
--- a/test/test_endpoint.py
+++ b/test/test_endpoint.py
@@ -4,6 +4,11 @@
import tableauserverclient as TSC
from tableauserverclient.server.endpoint import Endpoint
+from tableauserverclient.server.endpoint.exceptions import (
+ FailedSignInError,
+ NonXMLResponseError,
+ ServerResponseError,
+)
import requests_mock
@@ -91,3 +96,66 @@ def test_set_user_agent_when_blank(server: TSC.Server) -> None:
params = {"headers": {}} # type: ignore
result = Endpoint.set_user_agent(params)
assert result["headers"]["User-Agent"].startswith("Tableau Server Client")
+
+
+# --- ServerResponseError / FailedSignInError exception parsing (issue #1083) ---
+
+NS = {"t": "http://tableau.com/api"}
+
+STANDARD_ERROR_XML = b"""
+
+
+ Unauthorized Access
+ Invalid credentials were provided.
+
+"""
+
+NO_ERROR_ELEMENT_XML = b"""
+
+ Something went wrong but with no error element
+"""
+
+NOT_XML_CONTENT = b"Internal Server Error (not XML at all)"
+
+
+def test_server_response_error_standard_xml():
+ """Standard XML with a t:error element parses code/summary/detail correctly."""
+ err = ServerResponseError.from_response(STANDARD_ERROR_XML, NS, "http://test/")
+ assert err.code == "401002"
+ assert "Unauthorized" in err.summary
+ assert "Invalid credentials" in err.detail
+
+
+def test_server_response_error_no_error_element_does_not_raise():
+ """XML without a t:error element must not raise AttributeError (issue #1083)."""
+ err = ServerResponseError.from_response(NO_ERROR_ELEMENT_XML, NS, "http://test/")
+ assert err.code == ""
+ # The raw XML content should appear in summary/detail as the fallback
+ assert "Something went wrong" in err.summary or len(err.summary) > 0
+
+
+def test_server_response_error_not_xml_raises_parse_error():
+ """Non-XML content causes fromstring to raise a ParseError (not AttributeError)."""
+ import xml.etree.ElementTree as ET
+
+ with pytest.raises(ET.ParseError):
+ ServerResponseError.from_response(NOT_XML_CONTENT, NS, "http://test/")
+
+
+def test_failed_sign_in_error_no_error_element_does_not_raise():
+ """FailedSignInError shares from_response — same None guard must apply."""
+ err = FailedSignInError.from_response(NO_ERROR_ELEMENT_XML, NS, "http://test/")
+ assert err.code == ""
+ assert isinstance(err, FailedSignInError)
+
+
+def test_server_response_error_missing_summary_and_detail():
+ """XML with t:error but missing summary/detail children falls back gracefully."""
+ xml = b"""
+
+
+"""
+ err = ServerResponseError.from_response(xml, NS, "http://test/")
+ assert err.code == "500001"
+ assert err.summary == ""
+ assert err.detail == ""