Skip to content

Commit a77ae15

Browse files
committed
fix: add limits to ErrorInfo fields
- ErrorInfo is passed as FlightError.extra_info - In turn, extra_info contents are transported via gRPC headers - gRPC headers have hard limit of 16kB size (otherwise get gRPC Resource Exhausted: initial metadata too large) - added code to truncate `msg` and `detail` if necessary. Limit for each is 1kB - added code (breaking change) to limit `body` to 4kB - note: body is not used anywhere, and it is unlikely it is used 'in the wild' because as is it is not taken into account by the server (it will be used to pass polling info in the future) JIRA: CQ-1268
1 parent d355477 commit a77ae15

File tree

2 files changed

+63
-12
lines changed

2 files changed

+63
-12
lines changed

gooddata-flight-server/gooddata_flight_server/errors/error_info.py

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,33 @@
88

99
from gooddata_flight_server.errors.error_code import ErrorCode
1010

11+
_ERROR_INFO_MAX_MSG = 256
12+
_ERROR_INFO_MAX_DETAIL = 512
13+
_ERROR_INFO_MAX_BODY = 4096
14+
15+
16+
def _truncate_str_value(val: Optional[str], max_len: int) -> Optional[str]:
17+
if val is None:
18+
return None
19+
20+
if len(val) <= max_len:
21+
return val
22+
23+
# no big deal that the actual max length is slightly exceeded
24+
# all this truncating happens because ErrorInfo is eventually
25+
# passed via gRPC headers which have 16k hard limit
26+
return val[:max_len] + " [truncated]"
27+
28+
29+
def _body_of_allowed_size(val: Optional[bytes], max_len: int) -> Optional[bytes]:
30+
if val is None:
31+
return None
32+
33+
if len(val) > max_len:
34+
raise ValueError(f"ErrorInfo.body can be at most {max_len}B long.")
35+
36+
return val
37+
1138

1239
class ErrorInfo:
1340
"""
@@ -22,15 +49,15 @@ def __init__(
2249
body: Optional[bytes] = None,
2350
code: int = 0,
2451
) -> None:
25-
self._msg = msg
26-
self._detail: Optional[str] = detail
27-
self._body: Optional[bytes] = body
52+
self._msg = _truncate_str_value(msg, _ERROR_INFO_MAX_MSG)
53+
self._detail: Optional[str] = _truncate_str_value(detail, _ERROR_INFO_MAX_DETAIL)
54+
self._body: Optional[bytes] = _body_of_allowed_size(body, _ERROR_INFO_MAX_BODY)
2855
self._code: int = code
2956

3057
@property
3158
def msg(self) -> str:
3259
"""
33-
:return: human readable error message
60+
:return: human-readable error message
3461
"""
3562
return self._msg
3663

@@ -60,33 +87,37 @@ def with_msg(self, msg: str) -> "ErrorInfo":
6087
"""
6188
Updates error message.
6289
63-
:param msg: new message
90+
:param msg: new message, up to 256 characters; will be truncated if the limit is exceeded
6491
:return: self, for call chaining sakes
6592
"""
66-
self._msg = msg
93+
self._msg = _truncate_str_value(msg, _ERROR_INFO_MAX_MSG)
94+
6795
return self
6896

6997
def with_detail(self, detail: Optional[str] = None) -> "ErrorInfo":
7098
"""
7199
Updates or resets the error detail.
72100
73-
:param detail: detail to set; if None, the detail stored in the meta will be removed; default is None
101+
:param detail: detail to set; if None, the detail stored in the meta will be removed; default is None;
102+
detail can be up to 512 characters; will be truncated if the limit is exceeded
74103
:return: self, for call chaining sakes
75104
"""
76-
self._detail = detail
105+
self._detail = _truncate_str_value(detail, _ERROR_INFO_MAX_DETAIL)
106+
77107
return self
78108

79109
def with_body(self, body: Optional[Union[bytes, str]]) -> "ErrorInfo":
80110
"""
81111
Updates or resets the error body.
82112
113+
The body can be at most 4096 bytes long. If you try to set a larger body, a ValueError will
114+
be raised by this call.
115+
83116
:param body: body to set; if None, the body stored in the meta will be removed; default is None
84117
:return: self, for call chaining sakes
85118
"""
86-
if isinstance(body, str):
87-
self._body = body.encode("utf-8")
88-
else:
89-
self._body = body
119+
_body = body if isinstance(body, bytes) else body.encode("utf-8")
120+
self._body = _body_of_allowed_size(_body, _ERROR_INFO_MAX_BODY)
90121

91122
return self
92123

gooddata-flight-server/tests/errors/error_info.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# (C) 2024 GoodData Corporation
22
import pyarrow.flight
3+
import pytest
34
from gooddata_flight_server.errors.error_info import ErrorInfo, RetryInfo
45

56

@@ -22,6 +23,25 @@ def test_serde_error_info2():
2223
assert deserialized.detail == error.detail
2324

2425

26+
def test_error_info_limits1():
27+
error = ErrorInfo.for_reason(666, "error" * 500).with_detail("detail" * 500)
28+
29+
assert "truncated" in error.msg
30+
assert "truncated" in error.detail
31+
32+
error = ErrorInfo(code=666, msg="error" * 500, detail="detail" * 500)
33+
assert "truncated" in error.msg
34+
assert "truncated" in error.detail
35+
36+
37+
def test_error_info_limits2():
38+
with pytest.raises(ValueError):
39+
ErrorInfo.for_reason(666, "error").with_body(b"1234" * 2048)
40+
41+
with pytest.raises(ValueError):
42+
ErrorInfo(code=666, msg="error", body=b"1234" * 2048)
43+
44+
2545
def test_serde_retry_info1():
2646
retry = RetryInfo(
2747
flight_info=None,

0 commit comments

Comments
 (0)