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
15 changes: 9 additions & 6 deletions esrally/driver/driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -2129,34 +2129,37 @@ def _parse_headers(e):
# Some runners return a raw response, causing the 'error' property to be a string literal of the bytes/BytesIO object,
# we should avoid bubbling that up
# e.g. ApiError(413, '<_io.BytesIO object at 0xffffaf146a70>')
# errors="replace" so binary response bodies (e.g. OTLP ingest returns binary protobuf
# error frames) don't crash the driver with UnicodeDecodeError. Undecodable bytes become
# U+FFFD in the logged/recorded message.
if isinstance(e.body, bytes):
# could be an empty body
if error_body := e.body.decode("utf-8"):
if error_body := e.body.decode("utf-8", errors="replace"):
error_message = error_body
else:
# to be consistent with an empty 'e.error'
error_message = str(None)
elif isinstance(e.body, BytesIO):
# could be an empty body
if error_body := e.body.read().decode("utf-8"):
if error_body := e.body.read().decode("utf-8", errors="replace"):
error_message = error_body
else:
# to be consistent with an empty 'e.error'
error_message = str(None)
# fallback to 'error' property if the body isn't bytes/BytesIO
else:
if isinstance(e.error, bytes):
error_message = e.error.decode("utf-8")
error_message = e.error.decode("utf-8", errors="replace")
elif isinstance(e.error, BytesIO):
error_message = e.error.read().decode("utf-8")
error_message = e.error.read().decode("utf-8", errors="replace")
else:
# if the 'error' is empty, we get back str(None)
error_message = e.error

if isinstance(e.info, bytes):
error_info = e.info.decode("utf-8")
error_info = e.info.decode("utf-8", errors="replace")
elif isinstance(e.info, BytesIO):
error_info = e.info.read().decode("utf-8")
error_info = e.info.read().decode("utf-8", errors="replace")
else:
error_info = e.info

Expand Down
19 changes: 19 additions & 0 deletions tests/driver/driver_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -1987,6 +1987,25 @@ async def test_execute_single_with_http_400_with_raw_response_body(self):
await driver.execute_single(self.context_managed(runner), es, params, on_error=OnErrorBehavior.ABORT)
assert exc.value.args[0] == ("Request returned an error. Error type: api, Description: Huge error, HTTP Status: 499")

@pytest.mark.asyncio
async def test_execute_single_with_http_400_with_non_utf8_raw_response_body(self):
es = None
params = None
body = io.BytesIO(b"\xff")
str_literal = str(body)
error_meta = elastic_transport.ApiResponseMeta(
status=499,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot why 499 and not 400 like the test is called? A server is unlikely to return a 499, since that is typically used as a client timeout

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in 835a0e1: changed that test fixture and expectation to use HTTP 400 so it matches the test intent.

http_version="1.1",
headers=elastic_transport.HttpHeaders(),
duration=0.0,
node=elastic_transport.NodeConfig(scheme="http", host="localhost", port=9200),
)
runner = mock.AsyncMock(side_effect=elasticsearch.ApiError(message=str_literal, meta=error_meta, body=body))

with pytest.raises(exceptions.RallyAssertionError) as exc:
await driver.execute_single(self.context_managed(runner), es, params, on_error=OnErrorBehavior.ABORT)
assert exc.value.args[0] == ("Request returned an error. Error type: api, Description: �, HTTP Status: 499")

@pytest.mark.asyncio
async def test_execute_single_with_http_400(self):
es = None
Expand Down