From 2aa9554e0abc2a7d08f59a5a178954cd7735ecd9 Mon Sep 17 00:00:00 2001 From: Praveen Mittal Date: Fri, 24 Jul 2026 21:06:12 +0200 Subject: [PATCH] fix: handle non-string request bodies in error-logging round-trip dump RoundTrip.generate() passed request.body directly to _redacted_dump(), which assumes a str. For a streamed upload (a file-like object passed as data=), body is left as the original file object by requests, and _redacted_dump's len(body) call raised TypeError: object of type '...' has no len(). For a non-UTF8 bytes body, the JSONDecodeError fallback path called body.encode() on already-decoded bytes, raising AttributeError: 'bytes' object has no attribute 'encode'. Both crashes happened inside the "unable to parse response" debug-log path itself, masking the actual API error behind an unrelated Python traceback. Added _request_body_str() to normalize body to a string before it reaches _redacted_dump: str passes through, bytes/bytearray decode with errors="replace", and any other (stream/file-like) object is represented with a fixed "[stream body]" placeholder rather than read, since reading it here would consume the stream (which may already be at an unknown position after the actual send) with no guarantee its content is UTF-8 text. Fixes #1489 Signed-off-by: Praveen Mittal --- databricks/sdk/logger/round_trip_logger.py | 20 ++++++++- tests/test_errors.py | 47 +++++++++++++++++++++- 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/databricks/sdk/logger/round_trip_logger.py b/databricks/sdk/logger/round_trip_logger.py index 7ff9d55c9..78ad50969 100644 --- a/databricks/sdk/logger/round_trip_logger.py +++ b/databricks/sdk/logger/round_trip_logger.py @@ -44,7 +44,9 @@ 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("> ", self._request_body_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 @@ -54,6 +56,22 @@ def generate(self) -> str: sb.append(self._redacted_dump("< ", decoded)) return "\n".join(sb) + @staticmethod + def _request_body_str(body: Any) -> str: + """Normalize a `requests.PreparedRequest.body` into a string safe to hand to `_redacted_dump`. + + The body is usually `str` or `bytes`, but for a streamed upload (e.g. a file object passed as + `data=`) `requests` leaves it as the original file-like object. Reading it here for logging + purposes would consume the stream (which may already be at an unknown position after the actual + send) and isn't guaranteed to be UTF-8, so it's represented with a fixed placeholder instead of + being read. + """ + if isinstance(body, str): + return body + if isinstance(body, (bytes, bytearray)): + return bytes(body).decode("utf-8", errors="replace") + return "[stream body]" + @staticmethod def _mask(m: Dict[str, any]): for k in m: diff --git a/tests/test_errors.py b/tests/test_errors.py index d644979ab..5549c5349 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -1,4 +1,5 @@ import http.client +import io import json from dataclasses import dataclass, field from typing import Any, List, Optional @@ -24,13 +25,14 @@ def fake_raw_response( status_code: int, response_body: bytes, path: Optional[str] = None, + request_body: Any = None, ) -> requests.Response: resp = requests.Response() resp.status_code = status_code resp.reason = http.client.responses.get(status_code, "") if path is None: path = "/api/2.0/service" - resp.request = requests.Request(method, f"https://databricks.com{path}").prepare() + resp.request = requests.Request(method, f"https://databricks.com{path}", data=request_body).prepare() resp._content = response_body return resp @@ -355,6 +357,49 @@ class TestCase: want_err_type=errors.NotFound, want_message="unable to parse response. 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 https://github.com/databricks/databricks-sdk-py/issues. Request log:```GET /api/2.0/service\n< 404 Not Found\n< �```", ), + TestCase( + # A streamed upload (e.g. a file object passed as `data=`) leaves `request.body` as the + # original file-like object rather than str/bytes. Building the "unable to parse response" + # debug log used to crash with `TypeError: object of type '...' has no len()` trying to treat + # it as a string, masking the real API error entirely. + name="unable_to_parse_response_with_streamed_request_body", + response=fake_raw_response( + method="PUT", + status_code=400, + response_body=b"this is not a real response", + request_body=io.BytesIO(b"some file content"), + ), + want_err_type=errors.BadRequest, + want_message=( + "unable to parse response. 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 " + "https://github.com/databricks/databricks-sdk-py/issues. Request log:```PUT /api/2.0/service\n" + "> [stream body]\n" + "< 400 Bad Request\n" + "< this is not a real response```" + ), + ), + TestCase( + # A non-UTF8 bytes request body (e.g. binary file content) used to crash with + # `AttributeError: 'bytes' object has no attribute 'encode'` once JSON parsing failed on it, + # again masking the real API error. + name="unable_to_parse_response_with_non_utf8_bytes_request_body", + response=fake_raw_response( + method="PUT", + status_code=400, + response_body=b"this is not a real response", + request_body=b"\x80not json", + ), + want_err_type=errors.BadRequest, + want_message=( + "unable to parse response. 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 " + "https://github.com/databricks/databricks-sdk-py/issues. Request log:```PUT /api/2.0/service\n" + "> �not json\n" + "< 400 Bad Request\n" + "< this is not a real response```" + ), + ), ]