From 0c94dd871fbc069c1a38237d16e9c908b856b805 Mon Sep 17 00:00:00 2001 From: HardMax71 Date: Fri, 25 Sep 2026 17:14:19 +0200 Subject: [PATCH] fix(http): name the status code for an error without a body and end the get_object_value span An error response with a registered error class but no body gave "failed to deserialize: ". The message now ends in the status code, as in kiota-dotnet. The get_object_value span was started before the no-body check and ended only after a successful get_object_value, so it stayed open without a body and when deserialization raised; it is now started only when there is a body and ended in a finally. Fixes #755. --- .../httpx/kiota_http/httpx_request_adapter.py | 14 +++-- .../httpx/tests/test_httpx_request_adapter.py | 54 +++++++++++++++++-- 2 files changed, 56 insertions(+), 12 deletions(-) diff --git a/packages/http/httpx/kiota_http/httpx_request_adapter.py b/packages/http/httpx/kiota_http/httpx_request_adapter.py index 8fa85187..d81cf02a 100644 --- a/packages/http/httpx/kiota_http/httpx_request_adapter.py +++ b/packages/http/httpx/kiota_http/httpx_request_adapter.py @@ -487,16 +487,14 @@ async def _get_error_from_response( ) attribute_span.set_attribute(ERROR_BODY_FOUND_KEY, bool(root_node)) + if not root_node or not error_class: + return None _get_obj_ctx = trace.set_span_in_context(_throw_failed_resp_span) _get_obj_span = tracer.start_span("get_object_value", context=_get_obj_ctx) - - if not root_node: - return None - error = None - if error_class: - error = root_node.get_object_value(error_class) + try: + return root_node.get_object_value(error_class) + finally: _get_obj_span.end() - return error async def throw_failed_responses( self, @@ -575,7 +573,7 @@ async def throw_failed_responses( exc = APIError( ( "The server returned an unexpected status code and the error registered" - f" for this code failed to deserialize: {type(error)}" + f" for this code failed to deserialize: {response_status_code}" ), response_status_code, response_headers, # type: ignore diff --git a/packages/http/httpx/tests/test_httpx_request_adapter.py b/packages/http/httpx/tests/test_httpx_request_adapter.py index 5b508d2e..cdcbd36a 100644 --- a/packages/http/httpx/tests/test_httpx_request_adapter.py +++ b/packages/http/httpx/tests/test_httpx_request_adapter.py @@ -12,12 +12,13 @@ SerializationWriterFactoryRegistry, ) from opentelemetry import trace +from opentelemetry.sdk.trace import SpanProcessor, TracerProvider from kiota_http.httpx_request_adapter import HttpxRequestAdapter from kiota_http.middleware import REQUEST_OPTIONS_KEY from kiota_http.middleware.options import ResponseHandlerOption -from .helpers import MockResponseObject +from .helpers import MockErrorObject, MockResponseObject APPLICATION_JSON = "application/json" BASE_URL = "https://graph.microsoft.com" @@ -237,9 +238,54 @@ async def test_throw_failed_responses_not_apierror( span = mock_otel_span await request_adapter.throw_failed_responses(resp, mock_error_500_map, span, span) assert ( - "The server returned an unexpected status code and the error registered" - " for this code failed to deserialize" - ) in str(e.value.message) + str(e.value.message) == "The server returned an unexpected status code and the error" + " registered for this code failed to deserialize: 500" + ) + + +@pytest.mark.asyncio +async def test_throw_failed_responses_empty_error_body( + request_adapter, mock_apierror_XXX_map, mock_otel_span +): + resp = httpx.Response(status_code=503) + assert request_adapter.get_response_content_type(resp) is None + + with pytest.raises(APIError) as e: + span = mock_otel_span + await request_adapter.throw_failed_responses(resp, mock_apierror_XXX_map, span, span) + assert ( + str(e.value.message) == "The server returned an unexpected status code and the error" + " registered for this code failed to deserialize: 503" + ) + assert e.value.response_status_code == 503 + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "root_node", [ + None, + MockErrorObject, + Mock(get_object_value=Mock(side_effect=ValueError("unexpected value"))), + ], + ids=["no error body", "error body", "error body that fails to deserialize"] +) +async def test_throw_failed_responses_ends_every_span( + root_node, request_adapter, mock_apierror_XXX_map, mock_otel_span, monkeypatch +): + processor = Mock(spec=SpanProcessor) + provider = TracerProvider() + provider.add_span_processor(processor) + monkeypatch.setattr("kiota_http.httpx_request_adapter.tracer", provider.get_tracer(__name__)) + request_adapter.get_root_parse_node = AsyncMock(return_value=root_node) + resp = httpx.Response(status_code=503, headers={"Content-Type": "application/json"}) + + with pytest.raises(APIError): + await request_adapter.throw_failed_responses( + resp, mock_apierror_XXX_map, mock_otel_span, mock_otel_span + ) + started = [start.args[0] for start in processor.on_start.call_args_list] + assert "throw_failed_responses" in [s.name for s in started] + assert [s.name for s in started if s.end_time is None] == [] @pytest.mark.asyncio