Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions NEXT_CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
* Add native support for authentication through Azure DevOps OIDC

### Bug Fixes
* Fix bearer tokens logged in exception messages

### Documentation

Expand Down
5 changes: 4 additions & 1 deletion databricks/sdk/_base_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,10 @@ def __init__(
# Default to 60 seconds
self._http_timeout_seconds = http_timeout_seconds or 60

self._error_parser = _Parser(extra_error_customizers=extra_error_customizers)
self._error_parser = _Parser(
extra_error_customizers=extra_error_customizers,
debug_headers=debug_headers,
)

def _authenticate(self, r: requests.PreparedRequest) -> requests.PreparedRequest:
if self._header_factory:
Expand Down
11 changes: 8 additions & 3 deletions databricks/sdk/errors/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,15 @@
]


def _unknown_error(response: requests.Response) -> str:
def _unknown_error(response: requests.Response, debug_headers: bool = False) -> str:
"""A standard error message that can be shown when an API response cannot be parsed.

This error message includes a link to the issue tracker for the SDK for users to report the issue to us.

:param response: The response object from the API request.
:param debug_headers: Whether to include headers in the request log. Defaults to False for security.
"""
request_log = RoundTrip(response, debug_headers=True, debug_truncate_bytes=10 * 1024).generate()
request_log = RoundTrip(response, debug_headers=debug_headers, debug_truncate_bytes=10 * 1024).generate()
return (
"This is likely a bug in the Databricks SDK for Python or the underlying "
"API. Please report this issue with the following debugging information to the SDK issue tracker at "
Expand All @@ -56,11 +59,13 @@ def __init__(
self,
extra_error_parsers: List[_ErrorDeserializer] = [],
extra_error_customizers: List[_ErrorCustomizer] = [],
debug_headers: bool = False,
):
self._error_parsers = _error_deserializers + (extra_error_parsers if extra_error_parsers is not None else [])
self._error_customizers = _error_customizers + (
extra_error_customizers if extra_error_customizers is not None else []
)
self._debug_headers = debug_headers

def get_api_error(self, response: requests.Response) -> Optional[DatabricksError]:
"""
Expand All @@ -84,7 +89,7 @@ def get_api_error(self, response: requests.Response) -> Optional[DatabricksError
)
return _error_mapper(
response,
{"message": "unable to parse response. " + _unknown_error(response)},
{"message": "unable to parse response. " + _unknown_error(response, self._debug_headers)},
)

# Private link failures happen via a redirect to the login page. From a requests-perspective, the request
Expand Down
44 changes: 44 additions & 0 deletions tests/test_errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -371,3 +371,47 @@ def test_get_api_error(test_case: TestCase):
assert isinstance(e.value, test_case.want_err_type)
assert str(e.value) == test_case.want_message
assert e.value.get_error_details() == test_case.want_details


def test_debug_headers_disabled_by_default():
"""Test that debug_headers=False by default does not leak sensitive headers in unparseable errors."""
# Create a response with Authorization header that cannot be parsed
resp = requests.Response()
resp.status_code = 400
resp.reason = "Bad Request"
resp.request = requests.Request("POST", "https://databricks.com/api/2.0/sql/statements").prepare()
resp.request.headers["Authorization"] = "Bearer secret-token-12345"
resp.request.headers["X-Databricks-Azure-SP-Management-Token"] = "secret-azure-token-67890"
resp._content = b"unparseable response"

parser = errors._Parser(debug_headers=False)
error = parser.get_api_error(resp)

error_message = str(error)
# Verify that sensitive tokens are NOT in the error message
assert "secret-token-12345" not in error_message
assert "secret-azure-token-67890" not in error_message
assert "Authorization" not in error_message
assert "X-Databricks-Azure-SP-Management-Token" not in error_message


def test_debug_headers_enabled_shows_headers():
"""Test that debug_headers=True includes headers in unparseable error messages."""
# Create a response with Authorization header that cannot be parsed
resp = requests.Response()
resp.status_code = 400
resp.reason = "Bad Request"
resp.request = requests.Request("POST", "https://databricks.com/api/2.0/sql/statements").prepare()
resp.request.headers["Authorization"] = "Bearer debug-token-12345"
resp.request.headers["X-Databricks-Azure-SP-Management-Token"] = "debug-azure-token-67890"
resp._content = b"unparseable response"

parser = errors._Parser(debug_headers=True)
error = parser.get_api_error(resp)

error_message = str(error)
# Verify that headers ARE included when explicitly enabled
assert "Authorization" in error_message
assert "debug-token-12345" in error_message
assert "X-Databricks-Azure-SP-Management-Token" in error_message
assert "debug-azure-token-67890" in error_message
Loading