From 4e7af7f3d9fcbb364954ac3bcee56f12a7990240 Mon Sep 17 00:00:00 2001 From: Serena Ruan Date: Fri, 24 Oct 2025 18:05:22 +0800 Subject: [PATCH 1/2] fix errors Signed-off-by: Serena Ruan --- NEXT_CHANGELOG.md | 1 + databricks/sdk/errors/deserializer.py | 19 +++++ databricks/sdk/errors/parser.py | 3 +- databricks/sdk/logger/round_trip_logger.py | 2 +- tests/test_errors.py | 91 ++++++++++++++++++++++ 5 files changed, 114 insertions(+), 2 deletions(-) diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md index 7b7b0d33a..8e08aac38 100644 --- a/NEXT_CHANGELOG.md +++ b/NEXT_CHANGELOG.md @@ -3,6 +3,7 @@ ## Release v0.71.0 ### New Features and Improvements +* Add a new `_ProtobufErrorDeserializer` for handling Protobuf response errors. ### Bug Fixes diff --git a/databricks/sdk/errors/deserializer.py b/databricks/sdk/errors/deserializer.py index 5a6e0da09..2a82d4abf 100644 --- a/databricks/sdk/errors/deserializer.py +++ b/databricks/sdk/errors/deserializer.py @@ -117,3 +117,22 @@ def deserialize_error(self, response: requests.Response, response_body: bytes) - } logging.debug("_HtmlErrorParser: no
 tag found in error response")
         return None
+
+class _ProtobufErrorDeserializer(_ErrorDeserializer):
+    """
+    Parses errors from the Databricks REST API in Protobuf format.
+    """
+
+    def deserialize_error(self, response: requests.Response, response_body: bytes) -> Optional[dict]:
+        try:
+            from google.rpc import status_pb2
+
+            status = status_pb2.Status()
+            status.ParseFromString(response_body)
+            return {
+                "message": status.message,
+                "error_code": response.status_code,
+            }
+        except Exception as e:
+            logging.debug("_ProtobufErrorParser: unable to parse response as Protobuf", exc_info=e)
+            return None
diff --git a/databricks/sdk/errors/parser.py b/databricks/sdk/errors/parser.py
index 2fefc4e2f..40867f8ec 100644
--- a/databricks/sdk/errors/parser.py
+++ b/databricks/sdk/errors/parser.py
@@ -8,7 +8,7 @@
 from .customizer import _ErrorCustomizer, _RetryAfterCustomizer
 from .deserializer import (_EmptyDeserializer, _ErrorDeserializer,
                            _HtmlErrorDeserializer, _StandardErrorDeserializer,
-                           _StringErrorDeserializer)
+                           _StringErrorDeserializer, _ProtobufErrorDeserializer)
 from .mapper import _error_mapper
 from .private_link import (_get_private_link_validation_error,
                            _is_private_link_redirect)
@@ -21,6 +21,7 @@
     _StandardErrorDeserializer(),
     _StringErrorDeserializer(),
     _HtmlErrorDeserializer(),
+    _ProtobufErrorDeserializer(),
 ]
 
 # A list of _ErrorCustomizers that are applied to the error arguments after they are parsed. Customizers can modify the
diff --git a/databricks/sdk/logger/round_trip_logger.py b/databricks/sdk/logger/round_trip_logger.py
index 7ff9d55c9..44f71584e 100644
--- a/databricks/sdk/logger/round_trip_logger.py
+++ b/databricks/sdk/logger/round_trip_logger.py
@@ -44,7 +44,7 @@ def generate(self) -> str:
             for k, v in request.headers.items():
                 sb.append(f"> * {k}: {self._only_n_bytes(v, self._debug_truncate_bytes)}")
         if request.body:
-            sb.append("> [raw stream]" if self._raw else self._redacted_dump("> ", request.body))
+            sb.append("> [raw stream]" if self._raw else self._redacted_dump("> ", str(request.body)))
         sb.append(f"< {self._response.status_code} {self._response.reason}")
         if self._raw and self._response.headers.get("Content-Type", None) != "application/json":
             # Raw streams with `Transfer-Encoding: chunked` do not have `Content-Type` header
diff --git a/tests/test_errors.py b/tests/test_errors.py
index 57e045c3a..270796283 100644
--- a/tests/test_errors.py
+++ b/tests/test_errors.py
@@ -8,6 +8,7 @@
 
 from databricks.sdk import errors
 from databricks.sdk.errors import details
+from google.rpc import status_pb2
 
 
 def fake_response(
@@ -415,3 +416,93 @@ def test_debug_headers_enabled_shows_headers():
     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
+
+def test_protobuf_error_deserializer_valid_protobuf():
+    # Create a valid protobuf Status message
+    status = status_pb2.Status()
+    status.code = 3  # INVALID_ARGUMENT
+    status.message = "Invalid parameter provided"
+    serialized_status = status.SerializeToString()
+
+    resp = fake_raw_response(
+        method="POST",
+        status_code=400,
+        response_body=serialized_status,
+    )
+
+    parser = errors._Parser()
+    error = parser.get_api_error(resp)
+
+    assert isinstance(error, errors.BadRequest)
+    assert str(error) == "Invalid parameter provided"
+
+
+def test_protobuf_error_deserializer_invalid_protobuf():
+    # Create a response with invalid protobuf data that should fall through to other parsers
+    resp = fake_raw_response(
+        method="POST",
+        status_code=400,
+        response_body=b"\x00\x01\x02\x03\x04\x05",  # Invalid protobuf
+    )
+
+    parser = errors._Parser()
+    error = parser.get_api_error(resp)
+
+    # Should fall back to the generic error handler
+    assert isinstance(error, errors.BadRequest)
+    assert "unable to parse response" in str(error)
+
+
+def test_protobuf_error_deserializer_empty_message():
+    # Create a protobuf Status message with empty message
+    status = status_pb2.Status()
+    status.code = 5  # NOT_FOUND
+    status.message = ""
+    serialized_status = status.SerializeToString()
+
+    resp = fake_raw_response(
+        method="GET",
+        status_code=404,
+        response_body=serialized_status,
+    )
+
+    parser = errors._Parser()
+    error = parser.get_api_error(resp)
+
+    assert isinstance(error, errors.NotFound)
+    assert str(error) == "None"
+
+
+def test_protobuf_error_deserializer_with_details():
+    # Create a protobuf Status message with details
+    status = status_pb2.Status()
+    status.code = 9  # FAILED_PRECONDITION
+    status.message = "Resource is in an invalid state"
+    serialized_status = status.SerializeToString()
+
+    resp = fake_raw_response(
+        method="POST",
+        status_code=400,
+        response_body=serialized_status,
+    )
+
+    parser = errors._Parser()
+    error = parser.get_api_error(resp)
+
+    assert isinstance(error, errors.BadRequest)
+    assert str(error) == "Resource is in an invalid state"
+
+
+def test_protobuf_error_deserializer_priority():
+    resp = fake_valid_response(
+        method="POST",
+        status_code=400,
+        error_code="INVALID_REQUEST",
+        message="Invalid request body",
+    )
+
+    parser = errors._Parser()
+    error = parser.get_api_error(resp)
+
+    assert isinstance(error, errors.BadRequest)
+    assert str(error) == "Invalid request body"

From 8d88ef6d29a31bb3bb1ae7b31a815c07259b35f4 Mon Sep 17 00:00:00 2001
From: Serena Ruan 
Date: Fri, 24 Oct 2025 19:02:25 +0800
Subject: [PATCH 2/2] fmt

Signed-off-by: Serena Ruan 
---
 databricks/sdk/errors/deserializer.py | 1 +
 databricks/sdk/errors/parser.py       | 5 +++--
 tests/test_errors.py                  | 3 ++-
 3 files changed, 6 insertions(+), 3 deletions(-)

diff --git a/databricks/sdk/errors/deserializer.py b/databricks/sdk/errors/deserializer.py
index 2a82d4abf..b015d3359 100644
--- a/databricks/sdk/errors/deserializer.py
+++ b/databricks/sdk/errors/deserializer.py
@@ -118,6 +118,7 @@ def deserialize_error(self, response: requests.Response, response_body: bytes) -
         logging.debug("_HtmlErrorParser: no 
 tag found in error response")
         return None
 
+
 class _ProtobufErrorDeserializer(_ErrorDeserializer):
     """
     Parses errors from the Databricks REST API in Protobuf format.
diff --git a/databricks/sdk/errors/parser.py b/databricks/sdk/errors/parser.py
index 40867f8ec..6421ef80a 100644
--- a/databricks/sdk/errors/parser.py
+++ b/databricks/sdk/errors/parser.py
@@ -7,8 +7,9 @@
 from .base import DatabricksError
 from .customizer import _ErrorCustomizer, _RetryAfterCustomizer
 from .deserializer import (_EmptyDeserializer, _ErrorDeserializer,
-                           _HtmlErrorDeserializer, _StandardErrorDeserializer,
-                           _StringErrorDeserializer, _ProtobufErrorDeserializer)
+                           _HtmlErrorDeserializer, _ProtobufErrorDeserializer,
+                           _StandardErrorDeserializer,
+                           _StringErrorDeserializer)
 from .mapper import _error_mapper
 from .private_link import (_get_private_link_validation_error,
                            _is_private_link_redirect)
diff --git a/tests/test_errors.py b/tests/test_errors.py
index 270796283..907c81000 100644
--- a/tests/test_errors.py
+++ b/tests/test_errors.py
@@ -5,10 +5,10 @@
 
 import pytest
 import requests
+from google.rpc import status_pb2
 
 from databricks.sdk import errors
 from databricks.sdk.errors import details
-from google.rpc import status_pb2
 
 
 def fake_response(
@@ -417,6 +417,7 @@ def test_debug_headers_enabled_shows_headers():
     assert "X-Databricks-Azure-SP-Management-Token" in error_message
     assert "debug-azure-token-67890" in error_message
 
+
 def test_protobuf_error_deserializer_valid_protobuf():
     # Create a valid protobuf Status message
     status = status_pb2.Status()