Skip to content

Commit 07855fe

Browse files
committed
update dependencies
1 parent b5aed30 commit 07855fe

2 files changed

Lines changed: 239 additions & 1 deletion

File tree

pyproject.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ classifiers = [
2727
dependencies = [
2828
"requests>=2.28.1,<3",
2929
"google-auth~=2.0",
30-
"protobuf>=4.21.0,<7.0",
30+
# Exclude vulnerable protobuf versions: [,4.25.8), [5.26.0rc1, 5.29.5), [6.30.0rc1, 6.31.1)
31+
# https://security.snyk.io/vuln/SNYK-PYTHON-PROTOBUF-10364902
32+
"protobuf>=4.25.8,!=5.26.*,!=5.27.*,!=5.28.*,!=5.29.0,!=5.29.1,!=5.29.2,!=5.29.3,!=5.29.4,!=6.30.0,!=6.30.1,!=6.31.0,<7.0",
3133
]
3234

3335
[project.urls]

report.md

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
# Bug Report: TypeError in RoundTrip Logger
2+
3+
## Summary
4+
A `TypeError` occurs in the Databricks SDK Python library when the round trip logger attempts to call `len()` on a `_io.BytesIO` object. This happens during error handling when the SDK tries to generate debugging information for failed API requests.
5+
6+
## Error Details
7+
8+
**Error Type:** `TypeError: object of type '_io.BytesIO' has no len()`
9+
10+
**Stack Trace:**
11+
1. `databricks/sdk/_base_client.py:296` in `_perform()` method
12+
2. `databricks/sdk/errors/parser.py:87` in `get_api_error()` method
13+
3. `databricks/sdk/errors/parser.py:39` in `_unknown_error()` function
14+
4. `databricks/sdk/logger/round_trip_logger.py:47` in `generate()` method
15+
5. `databricks/sdk/logger/round_trip_logger.py:113` in `_redacted_dump()` method
16+
17+
## Root Cause Analysis
18+
19+
### The Immediate Bug
20+
The `_redacted_dump()` method in `round_trip_logger.py` assumes that the `body` parameter is always a string and calls `len(body)` directly on line 113:
21+
22+
```python
23+
def _redacted_dump(self, prefix: str, body: str) -> str:
24+
if len(body) == 0: # ← This line causes the error
25+
return ""
26+
```
27+
28+
### Why BytesIO Objects Are Present
29+
In `_base_client.py` lines 167-169, the SDK wraps string and bytes data in `BytesIO` objects for retry functionality:
30+
31+
```python
32+
# Wrap strings and bytes in a seekable stream so that we can rewind them.
33+
if isinstance(data, (str, bytes)):
34+
data = io.BytesIO(data.encode("utf-8") if isinstance(data, str) else data)
35+
```
36+
37+
When a request is made, this `BytesIO` object becomes the `request.body` in the requests library, which is then passed to the logging system when an error occurs.
38+
39+
### When the Error Occurs
40+
This error happens when:
41+
1. An API request contains a request body (originally string or bytes)
42+
2. The request fails or returns an unparseable response
43+
3. The error parser triggers `_unknown_error()` to generate debug information
44+
4. The `RoundTrip` logger tries to log the request body
45+
5. The `_redacted_dump()` method receives a `BytesIO` object instead of a string
46+
47+
### **LIKELY Root Cause: Protocol Mismatch - Sending JSON to RPC/Protobuf Endpoint**
48+
49+
**The SDK ONLY sends JSON, but may be hitting an endpoint that expects protobuf/RPC format.**
50+
51+
Critical evidence:
52+
- **Line 290 in `_base_client.py`:** Uses `json=body` which ALWAYS serializes requests as JSON
53+
- **All API calls:** Set `Content-Type: application/json` and `Accept: application/json`
54+
- **No protobuf serialization:** SDK has ZERO code to serialize requests as protobuf (no `SerializeToString` calls)
55+
- **Protobuf only for internal types:** Only uses protobuf for `Duration`/`Timestamp` which are JSON-serialized
56+
- **All error deserializers:** Expect text-based responses (JSON, HTML, or plain text with UTF-8)
57+
- `google.rpc.status_pb2` binary module is **NOT available** in the SDK environment
58+
59+
**Most Likely Scenario:**
60+
61+
```
62+
1. SDK sends JSON request to endpoint that expects protobuf/RPC
63+
2. Server rejects the request (400 Bad Request or 415 Unsupported Media Type)
64+
3. Server returns error response in protobuf binary format (or other non-JSON format)
65+
4. SDK tries to parse the error response as JSON → ALL parsers fail
66+
5. SDK triggers _unknown_error() to log debug info
67+
6. BytesIO bug prevents logging → user sees TypeError instead of protocol mismatch error
68+
```
69+
70+
**Alternative scenarios:**
71+
72+
- **Correct response in protobuf:** Server accepts JSON but responds in protobuf (unlikely - violates HTTP content negotiation)
73+
- **Gateway/proxy errors:** Intermediate systems returning binary/non-JSON errors
74+
- **Malformed response:** Response is corrupt, truncated, or has encoding issues
75+
76+
**The key insight:** We're sending the WRONG format to the server, and the error response we get back is also in a format we can't parse. The BytesIO bug masks what would otherwise reveal the protocol mismatch.
77+
78+
## Technical Impact
79+
80+
- **Error Masking:** The original API error is masked by this logging error, making debugging difficult
81+
- **User Experience:** Users see a confusing `TypeError` instead of the actual API error
82+
- **Debug Information Loss:** No useful debugging information is generated when this occurs
83+
84+
## Affected Components
85+
86+
- **Primary:** `databricks/sdk/logger/round_trip_logger.py` - The `_redacted_dump()` method
87+
- **Secondary:** `databricks/sdk/_base_client.py` - The data wrapping logic that creates BytesIO objects
88+
- **Impact:** Error handling and debugging across the entire SDK
89+
90+
## Recommended Fixes
91+
92+
### Fix 1: Handle BytesIO in Logger (Immediate)
93+
94+
The `_redacted_dump()` method should handle both string and `BytesIO` objects. Here's the recommended approach:
95+
96+
1. **Type checking:** Check if the body is a `BytesIO` object
97+
2. **Content extraction:** If it's `BytesIO`, read its contents and get the length appropriately
98+
3. **Rewind position:** Ensure the `BytesIO` position is reset after reading
99+
100+
Example fix for line 113 in `round_trip_logger.py`:
101+
```python
102+
def _redacted_dump(self, prefix: str, body) -> str: # Remove str type hint
103+
# Handle both str and BytesIO objects
104+
if isinstance(body, io.BytesIO):
105+
current_pos = body.tell() # Save current position
106+
body.seek(0) # Go to start
107+
content = body.read().decode('utf-8', errors='replace')
108+
body.seek(current_pos) # Restore position
109+
if len(content) == 0:
110+
return ""
111+
body_to_process = content
112+
else:
113+
if len(body) == 0:
114+
return ""
115+
body_to_process = body
116+
117+
# Continue with existing logic using body_to_process
118+
```
119+
120+
### Fix 2: Improve Binary Response Handling (Investigate Root Cause)
121+
122+
Add a fallback deserializer that provides better error messages for binary/unparseable responses:
123+
124+
Create a new `_BinaryResponseDeserializer` in `deserializer.py`:
125+
```python
126+
class _BinaryResponseDeserializer(_ErrorDeserializer):
127+
"""Handles binary or unparseable responses by providing diagnostic information."""
128+
129+
def deserialize_error(self, response: requests.Response, response_body: bytes) -> Optional[dict]:
130+
# This is a catch-all for responses that couldn't be parsed by other deserializers
131+
# Always return a result to provide diagnostic information
132+
133+
content_type = response.headers.get('Content-Type', 'unknown')
134+
content_preview = response_body[:100].hex() if len(response_body) > 0 else "empty"
135+
136+
# Try to determine if it's binary
137+
try:
138+
response_body.decode('utf-8')
139+
is_binary = False
140+
except UnicodeDecodeError:
141+
is_binary = True
142+
143+
message = (
144+
f"Unable to parse response. "
145+
f"Content-Type: {content_type}, "
146+
f"Length: {len(response_body)} bytes, "
147+
f"Binary: {is_binary}, "
148+
f"Preview (hex): {content_preview}"
149+
)
150+
151+
return {
152+
"message": message,
153+
"error_code": "UNPARSEABLE_RESPONSE",
154+
}
155+
```
156+
157+
Add it as the **last** deserializer in `parser.py`:
158+
```python
159+
_error_deserializers = [
160+
_EmptyDeserializer(),
161+
_StandardErrorDeserializer(),
162+
_StringErrorDeserializer(),
163+
_HtmlErrorDeserializer(),
164+
_BinaryResponseDeserializer(), # Catch-all, must be last
165+
]
166+
```
167+
168+
This will prevent the `_unknown_error()` path from being triggered and provide diagnostic information about what the response actually contains.
169+
170+
## Testing Recommendations
171+
172+
1. **Unit tests for BytesIO handling:** Add tests that pass `BytesIO` objects to `_redacted_dump()`
173+
2. **Integration tests:** Test error scenarios with request bodies containing string/bytes data
174+
3. **Edge cases:** Test with empty `BytesIO` objects and various encodings
175+
4. **Binary response testing:** Create test cases with binary/non-UTF8 responses
176+
5. **Content-Type validation:** Test responses with unexpected content types (e.g., `application/octet-stream`)
177+
6. **Malformed response testing:** Test with truncated JSON, invalid encoding, etc.
178+
179+
## Investigation Required
180+
181+
To confirm the protocol mismatch hypothesis:
182+
1. **Apply Fix 1 first:** This will allow logging to work and reveal response details
183+
2. **Check response headers:** Look for `Content-Type: application/x-protobuf` or `application/grpc`
184+
3. **Inspect response bytes:** Check if response starts with protobuf binary markers
185+
4. **Check HTTP status code:** Look for 400 (Bad Request) or 415 (Unsupported Media Type)
186+
5. **Identify the endpoint:** Determine which Databricks API endpoint is being called
187+
6. **Verify endpoint requirements:** Check if the endpoint has both REST/JSON and gRPC/protobuf versions
188+
7. **Review request URL:** Ensure using the correct URL path for REST/JSON API
189+
190+
## If Protocol Mismatch is Confirmed
191+
192+
**The SDK does NOT support protobuf/gRPC requests.** It is a REST/JSON-only SDK.
193+
194+
If the endpoint requires protobuf:
195+
1. **Check for REST alternative:** Most Databricks APIs have REST/JSON endpoints
196+
2. **Verify endpoint URL:** Ensure you're calling the REST API path, not a gRPC endpoint
197+
3. **Contact Databricks support:** If only protobuf is available, request REST/JSON support
198+
4. **Use different SDK:** Consider if there's a gRPC-compatible SDK available
199+
5. **Consider feature request:** File an issue to add protobuf support to the Python SDK
200+
201+
## Priority
202+
**Critical** - This bug has multiple levels:
203+
- **Immediate:** Prevents proper error reporting and debugging (Fix 1) - **REQUIRED**
204+
- **Diagnostics:** Add better error messages for unparseable responses (Fix 2) - **Recommended**
205+
- **Root cause:** Likely protocol mismatch - SDK sending JSON to protobuf endpoint - **Needs investigation**
206+
207+
## Files to Modify
208+
- `databricks/sdk/logger/round_trip_logger.py` (Fix 1 - immediate, required)
209+
- `databricks/sdk/errors/deserializer.py` (Fix 2 - add binary response handler, recommended)
210+
- `databricks/sdk/errors/parser.py` (Fix 2 - register new deserializer, recommended)
211+
- Add test cases in relevant test files
212+
213+
## Additional Notes
214+
- **Fix 1** should maintain backward compatibility with string inputs
215+
- Consider adding type hints that accurately reflect the method can accept both strings and BytesIO
216+
- The position management for BytesIO is crucial to avoid affecting retry logic
217+
- **Fix 2** provides diagnostic information for unparseable responses instead of failing silently
218+
- The SDK handles `google.rpc` types as JSON structures (via `@type` fields), not binary protobuf
219+
- `google.rpc.status_pb2` is NOT available in the SDK environment (verified)
220+
221+
---
222+
223+
## Summary
224+
225+
**What happened:** User got a `TypeError: object of type '_io.BytesIO' has no len()` instead of seeing the actual API error.
226+
227+
**Why it happened:**
228+
1. **Immediate cause:** Logger tried to call `len()` on a `BytesIO` object
229+
2. **Underlying cause:** SDK likely sent JSON to an endpoint expecting protobuf, got unparseable error response
230+
3. **SDK limitation:** SDK is REST/JSON-only, has NO protobuf serialization capability
231+
232+
**What needs to be done:**
233+
1. **Fix the logger** (Fix 1) - Required to see actual errors
234+
2. **Add binary response handler** (Fix 2) - Recommended for better diagnostics
235+
3. **Investigate the endpoint** - Verify if protocol mismatch is the root cause
236+
4. **User action:** Ensure using correct REST/JSON endpoint, not gRPC/protobuf endpoint

0 commit comments

Comments
 (0)