Skip to content

Commit 53fd47c

Browse files
fix(bedrock): raise_for_status in WrappedBotoClient so gateway errors surface (#95)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent cc7a3bf commit 53fd47c

4 files changed

Lines changed: 82 additions & 9 deletions

File tree

packages/uipath_langchain_client/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
All notable changes to `uipath_langchain_client` will be documented in this file.
44

5+
## [1.14.1] - 2026-06-23
6+
7+
### Fixed
8+
- `WrappedBotoClient` (the httpx-backed Bedrock shim) now calls `raise_for_status()` in `converse`, `invoke_model`, and the streaming generator before reading the response. Previously a non-2xx gateway response (e.g. 403 License-not-available) was parsed as a normal result and handed to `langchain_aws`, which raised a misleading `ValueError("No 'output' key found in the response from the Bedrock Converse API ... misconfiguration of endpoint or region")` — the real status code and `detail` were lost. Gateway HTTP errors now surface as the patched `UiPathAPIError` subclass (e.g. `UiPathPermissionDeniedError`), matching the OpenAI and Vertex paths. For streaming responses the error body is read first so the typed exception retains its `detail`.
9+
510
## [1.14.0] - 2026-06-15
611

712
### Added
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
__title__ = "UiPath LangChain Client"
22
__description__ = "A Python client for interacting with UiPath's LLM services via LangChain."
3-
__version__ = "1.14.0"
3+
__version__ = "1.14.1"

packages/uipath_langchain_client/src/uipath_langchain_client/clients/bedrock/utils.py

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,12 @@ def _stream_generator(
5757
if self.httpx_client is None:
5858
raise ValueError("httpx_client is not set")
5959
with self.httpx_client.stream("POST", "/", json=_serialize_bytes(request_body)) as response:
60+
if response.is_error:
61+
# The gateway returns a non-streamed JSON error body; read it so
62+
# the patched raise_for_status surfaces it (with detail) instead
63+
# of the EventStreamBuffer choking on a non-event payload.
64+
response.read()
65+
response.raise_for_status()
6066
buffer = EventStreamBuffer()
6167
for chunk in response.iter_bytes():
6268
buffer.add_data(chunk)
@@ -71,12 +77,12 @@ def _stream_generator(
7177
def invoke_model(self, **kwargs: Any) -> Any:
7278
if self.httpx_client is None:
7379
raise ValueError("httpx_client is not set")
74-
return {
75-
"body": self.httpx_client.post(
76-
"/",
77-
json=json.loads(kwargs.get("body", "{}")),
78-
)
79-
}
80+
response = self.httpx_client.post(
81+
"/",
82+
json=json.loads(kwargs.get("body", "{}")),
83+
)
84+
response.raise_for_status()
85+
return {"body": response}
8086

8187
def invoke_model_with_response_stream(self, **kwargs: Any) -> Any:
8288
return {"body": self._stream_generator(json.loads(kwargs.get("body", "{}")))}
@@ -90,7 +96,7 @@ def converse(
9096
) -> Any:
9197
if self.httpx_client is None:
9298
raise ValueError("httpx_client is not set")
93-
return self.httpx_client.post(
99+
response = self.httpx_client.post(
94100
"/",
95101
json=_serialize_bytes(
96102
{
@@ -99,7 +105,9 @@ def converse(
99105
**params,
100106
}
101107
),
102-
).json()
108+
)
109+
response.raise_for_status()
110+
return response.json()
103111

104112
def converse_stream(
105113
self,
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
"""Unit tests for ``WrappedBotoClient`` HTTP error surfacing.
2+
3+
The shim talks to the LLM Gateway over httpx instead of AWS. It must call
4+
``raise_for_status()`` so gateway HTTP errors (e.g. 403 License-not-available)
5+
propagate as exceptions, rather than being parsed as a normal result and then
6+
mis-reported downstream (langchain_aws raises a misleading "No 'output' key"
7+
``ValueError`` when the response lacks the expected fields).
8+
"""
9+
10+
import json
11+
12+
import httpx
13+
import pytest
14+
from uipath_langchain_client.clients.bedrock.utils import WrappedBotoClient
15+
16+
_ERROR_BODY = {
17+
"title": "License not available",
18+
"status": 403,
19+
"detail": "License not available for LLM usage.",
20+
}
21+
22+
23+
def _wrapped(handler: object) -> WrappedBotoClient:
24+
transport = httpx.MockTransport(handler) # type: ignore[arg-type]
25+
return WrappedBotoClient(
26+
httpx_client=httpx.Client(transport=transport, base_url="http://gateway")
27+
)
28+
29+
30+
def test_converse_raises_on_http_error() -> None:
31+
client = _wrapped(lambda request: httpx.Response(403, json=_ERROR_BODY))
32+
with pytest.raises(httpx.HTTPStatusError):
33+
client.converse(messages=[{"role": "user", "content": [{"text": "hi"}]}])
34+
35+
36+
def test_converse_returns_body_on_success() -> None:
37+
payload = {"output": {"message": {"role": "assistant", "content": [{"text": "ok"}]}}}
38+
client = _wrapped(lambda request: httpx.Response(200, json=payload))
39+
assert client.converse(messages=[]) == payload
40+
41+
42+
def test_invoke_model_raises_on_http_error() -> None:
43+
client = _wrapped(lambda request: httpx.Response(403, json=_ERROR_BODY))
44+
with pytest.raises(httpx.HTTPStatusError):
45+
client.invoke_model(body=json.dumps({"prompt": "hi"}))
46+
47+
48+
def test_converse_stream_raises_on_http_error() -> None:
49+
# The generator defers work until iterated, so the error surfaces on consume.
50+
client = _wrapped(lambda request: httpx.Response(403, json=_ERROR_BODY))
51+
stream = client.converse_stream(messages=[])["stream"]
52+
with pytest.raises(httpx.HTTPStatusError):
53+
list(stream)
54+
55+
56+
def test_invoke_model_with_response_stream_raises_on_http_error() -> None:
57+
client = _wrapped(lambda request: httpx.Response(403, json=_ERROR_BODY))
58+
stream = client.invoke_model_with_response_stream(body=json.dumps({"prompt": "hi"}))["body"]
59+
with pytest.raises(httpx.HTTPStatusError):
60+
list(stream)

0 commit comments

Comments
 (0)