Skip to content

Commit ac8c902

Browse files
authored
fix: Truncate HttpError message to first 8192 characters (#5020)
Signed-off-by: Phillippe Siclait <[email protected]>
1 parent 88abc78 commit ac8c902

File tree

3 files changed

+49
-15
lines changed

3 files changed

+49
-15
lines changed

lib/bindings/python/rust/http.rs

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,12 @@ impl HttpAsyncEngine {
138138
}
139139
}
140140

141+
#[derive(FromPyObject)]
142+
struct HttpError {
143+
code: u16,
144+
message: String,
145+
}
146+
141147
#[async_trait]
142148
impl<Req, Resp> AsyncEngine<SingleIn<Req>, ManyOut<Annotated<Resp>>, Error> for HttpAsyncEngine
143149
where
@@ -153,18 +159,10 @@ where
153159
Err(e) => {
154160
if let Some(py_err) = e.downcast_ref::<PyErr>() {
155161
Python::with_gil(|py| {
156-
let err_val = py_err.clone_ref(py).into_value(py);
157-
let bound_err = err_val.bind(py);
158-
159-
// check: Py03 exceptions cannot be cross-compiled, so we duck-type by name
160-
// and fields.
161-
if let Ok(type_name) = bound_err.get_type().name()
162-
&& type_name.to_string().contains("HttpError")
163-
&& let (Ok(code), Ok(message)) =
164-
(bound_err.getattr("code"), bound_err.getattr("message"))
165-
&& let (Ok(code), Ok(message)) =
166-
(code.extract::<u16>(), message.extract::<String>())
167-
{
162+
// With the Stable ABI, we can't subclass Python's built-in exceptions in PyO3, so instead we
163+
// implement the exception in Python and assume that it's an HttpError if the code and message
164+
// are present.
165+
if let Ok(HttpError { code, message }) = py_err.value(py).extract() {
168166
// SSE panics if there are carriage returns or newlines
169167
let message = message.replace(['\r', '\n'], "");
170168
return Err(http_error::HttpError { code, message })?;

lib/bindings/python/src/dynamo/llm/exceptions.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,33 @@
33

44
# flake8: noqa
55

6+
import logging
7+
8+
logger = logging.getLogger(__name__)
9+
10+
_MAX_MESSAGE_LENGTH = 8192
11+
612

713
class HttpError(Exception):
814
def __init__(self, code: int, message: str):
9-
if not (isinstance(code, int) and 0 <= code < 600):
15+
# These ValueErrors are easier to trace to here than the TypeErrors that
16+
# would be raised otherwise.
17+
if not isinstance(code, int) or isinstance(code, bool):
18+
raise ValueError("HttpError status code must be an integer")
19+
20+
if not isinstance(message, str):
21+
raise ValueError("HttpError message must be a string")
22+
23+
if not (0 <= code < 600):
1024
raise ValueError("HTTP status code must be an integer between 0 and 599")
11-
if not (isinstance(message, str) and 0 < len(message) <= 8192):
12-
raise ValueError("HTTP error message must be a string of length <= 8192")
25+
26+
if len(message) > _MAX_MESSAGE_LENGTH:
27+
logger.warning(
28+
f"HttpError message length {len(message)} exceeds max length {_MAX_MESSAGE_LENGTH}, truncating..."
29+
)
30+
message = message[: (_MAX_MESSAGE_LENGTH - 3)] + "..."
31+
1332
self.code = code
1433
self.message = message
34+
1535
super().__init__(f"HTTP {code}: {message}")

lib/bindings/python/tests/test_http_error.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,19 @@ def test_raise_http_error():
1818
def test_invalid_http_error_code():
1919
with pytest.raises(ValueError):
2020
HttpError(1700, "Invalid Code")
21+
22+
23+
def test_invalid_http_error_message():
24+
with pytest.raises(ValueError):
25+
# The second argument must be a string, not bytes.
26+
HttpError(400, b"Bad Request")
27+
28+
29+
def test_long_http_error_message():
30+
message = ("A" * 8192) + "B"
31+
error = HttpError(400, message)
32+
assert len(error.message) == 8192
33+
34+
# Ensure the exception string uses the truncated message too.
35+
assert message[:8189] in str(error)
36+
assert "B" not in str(error)

0 commit comments

Comments
 (0)