From 421e2ceeee4a7275aa4e4ebbe5ae00f92b7d4db2 Mon Sep 17 00:00:00 2001 From: Mason Daugherty Date: Fri, 14 Nov 2025 09:05:29 -0500 Subject: [PATCH] fix(core): don't mask exceptions (#33959) --- .../language_models/chat_models.py | 5 +- .../language_models/chat_models/test_base.py | 91 +++++++++++++++++++ 2 files changed, 95 insertions(+), 1 deletion(-) diff --git a/libs/core/langchain_core/language_models/chat_models.py b/libs/core/langchain_core/language_models/chat_models.py index f37113b4661..bfd37ea5883 100644 --- a/libs/core/langchain_core/language_models/chat_models.py +++ b/libs/core/langchain_core/language_models/chat_models.py @@ -91,7 +91,10 @@ def _generate_response_from_error(error: BaseException) -> list[ChatGeneration]: try: metadata["body"] = response.json() except Exception: - metadata["body"] = getattr(response, "text", None) + try: + metadata["body"] = getattr(response, "text", None) + except Exception: + metadata["body"] = None if hasattr(response, "headers"): try: metadata["headers"] = dict(response.headers) diff --git a/libs/core/tests/unit_tests/language_models/chat_models/test_base.py b/libs/core/tests/unit_tests/language_models/chat_models/test_base.py index b20623ad6fb..e0a2cb291c9 100644 --- a/libs/core/tests/unit_tests/language_models/chat_models/test_base.py +++ b/libs/core/tests/unit_tests/language_models/chat_models/test_base.py @@ -18,6 +18,7 @@ from langchain_core.language_models import ( ParrotFakeChatModel, ) from langchain_core.language_models._utils import _normalize_messages +from langchain_core.language_models.chat_models import _generate_response_from_error from langchain_core.language_models.fake_chat_models import ( FakeListChatModelError, GenericFakeChatModel, @@ -1234,3 +1235,93 @@ def test_model_profiles() -> None: model = MyModel(messages=iter([])) profile = model.profile assert profile + + +class MockResponse: + """Mock response for testing _generate_response_from_error.""" + + def __init__( + self, + status_code: int = 400, + headers: dict[str, str] | None = None, + json_data: dict[str, Any] | None = None, + json_raises: type[Exception] | None = None, + text_raises: type[Exception] | None = None, + ): + self.status_code = status_code + self.headers = headers or {} + self._json_data = json_data + self._json_raises = json_raises + self._text_raises = text_raises + + def json(self) -> dict[str, Any]: + if self._json_raises: + msg = "JSON parsing failed" + raise self._json_raises(msg) + return self._json_data or {} + + @property + def text(self) -> str: + if self._text_raises: + msg = "Text access failed" + raise self._text_raises(msg) + return "" + + +class MockAPIError(Exception): + """Mock API error with response attribute.""" + + def __init__(self, message: str, response: MockResponse | None = None): + super().__init__(message) + self.message = message + if response is not None: + self.response = response + + +def test_generate_response_from_error_with_valid_json() -> None: + """Test `_generate_response_from_error` with valid JSON response.""" + response = MockResponse( + status_code=400, + headers={"content-type": "application/json"}, + json_data={"error": {"message": "Bad request", "type": "invalid_request"}}, + ) + error = MockAPIError("API Error", response=response) + + generations = _generate_response_from_error(error) + + assert len(generations) == 1 + generation = generations[0] + assert isinstance(generation, ChatGeneration) + assert isinstance(generation.message, AIMessage) + assert generation.message.content == "" + + metadata = generation.message.response_metadata + assert metadata["body"] == { + "error": {"message": "Bad request", "type": "invalid_request"} + } + assert metadata["headers"] == {"content-type": "application/json"} + assert metadata["status_code"] == 400 + + +def test_generate_response_from_error_handles_streaming_response_failure() -> None: + # Simulates scenario where accessing response.json() or response.text + # raises ResponseNotRead on streaming responses + response = MockResponse( + status_code=400, + headers={"content-type": "application/json"}, + json_raises=Exception, # Simulates ResponseNotRead or similar + text_raises=Exception, + ) + error = MockAPIError("API Error", response=response) + + # This should NOT raise an exception, but should handle it gracefully + generations = _generate_response_from_error(error) + + assert len(generations) == 1 + generation = generations[0] + metadata = generation.message.response_metadata + + # When both fail, body should be None instead of raising an exception + assert metadata["body"] is None + assert metadata["headers"] == {"content-type": "application/json"} + assert metadata["status_code"] == 400