Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
20 changes: 19 additions & 1 deletion databricks/sdk/logger/round_trip_logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down
47 changes: 46 additions & 1 deletion tests/test_errors.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import http.client
import io
import json
from dataclasses import dataclass, field
from typing import Any, List, Optional
Expand All @@ -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

Expand Down Expand Up @@ -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```"
),
),
]


Expand Down
Loading