Skip to content

Commit 6186d2d

Browse files
authored
fix: v3 error response handling (#197)
1 parent f6d42a6 commit 6186d2d

4 files changed

Lines changed: 119 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
### Bug Fixes
1010

1111
1. [#194](https://github.com/InfluxCommunity/influxdb3-python/pull/194): Fix `InfluxDBClient3.write_file()` and `InfluxDBClient3.write_dataframe()` fail with batching mode.
12+
1. [#197](https://github.com/InfluxCommunity/influxdb3-python/pull/197): InfluxDB 3 Core/Enterprise write errors details handling.
1213

1314
## 0.17.0 [2026-01-08]
1415

influxdb_client_3/exceptions/exceptions.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,41 @@ def _get_message(self, response):
6363
def get(d, key):
6464
if not key or d is None:
6565
return d
66+
if not isinstance(d, dict):
67+
return None
6668
return get(d.get(key[0]), key[1:])
6769
try:
6870
node = json.loads(response.data)
71+
if isinstance(node, dict):
72+
# InfluxDB v3 error format: { "code": "...", "message": "..." }
73+
code = node.get("code")
74+
message = node.get("message")
75+
if message:
76+
return f"{code}: {message}" if code else message
77+
# InfluxDB v3 write error format:
78+
# {
79+
# "error": "...",
80+
# "data": [ { "error_message": "...", "line_number": 2, "original_line": "..." }, ... ]
81+
# }
82+
error_text = node.get("error")
83+
data = node.get("data")
84+
if error_text and isinstance(data, list):
85+
details = []
86+
for item in data:
87+
if not isinstance(item, dict):
88+
continue
89+
line_number = item.get("line_number")
90+
error_message = item.get("error_message")
91+
original_line = item.get("original_line")
92+
if line_number is not None and error_message and original_line:
93+
details.append(
94+
f"\tline {line_number}: {error_message} ({original_line})"
95+
)
96+
elif error_message:
97+
details.append(f"\t{error_message}")
98+
if details:
99+
return error_text + ":\n" + "\n".join(details)
100+
return error_text
69101
for key in [['message'], ['data', 'error_message'], ['error']]:
70102
value = get(node, key)
71103
if value is not None:

tests/test_api_client.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,58 @@ def test_api_error_unknown(self):
124124
self._test_api_error(response_body)
125125
self.assertEqual(response_body, err.exception.message)
126126

127+
def test_api_error_v3_with_detail(self):
128+
cases = [
129+
# all details available
130+
(
131+
"two-line details",
132+
'{"error":"partial write of line protocol occurred","data":['
133+
'{"error_message":"invalid column type for column \'v\', expected iox::column_type::field::float, '
134+
'got iox::column_type::field::uinteger","line_number":2,"original_line":"**.DBG.remote_***"},'
135+
'{"error_message":"invalid column type for column \'v\', expected iox::column_type::field::float, '
136+
'got iox::column_type::field::uinteger","line_number":3,"original_line":"***.INF.remote_***"}'
137+
']}',
138+
"partial write of line protocol occurred:\n"
139+
"\tline 2: invalid column type for column 'v', expected iox::column_type::field::float, "
140+
"got iox::column_type::field::uinteger (**.DBG.remote_***)\n"
141+
"\tline 3: invalid column type for column 'v', expected iox::column_type::field::float, "
142+
"got iox::column_type::field::uinteger (***.INF.remote_***)",
143+
),
144+
# error_message only (no line_number/original_line)
145+
(
146+
"message-only detail",
147+
'{"error":"partial write of line protocol occurred","data":['
148+
'{"error_message":"only error message"}]}',
149+
"partial write of line protocol occurred:\n"
150+
"\tonly error message",
151+
),
152+
# non-dict item in data list is skipped
153+
(
154+
"non-dict item skipped",
155+
'{"error":"partial write of line protocol occurred","data":[null,'
156+
'{"error_message":"bad line","line_number":2,"original_line":"bad lp"}]}',
157+
"partial write of line protocol occurred:\n"
158+
"\tline 2: bad line (bad lp)",
159+
),
160+
# details empty -> return error_text
161+
(
162+
"no detail fields",
163+
'{"error":"partial write of line protocol occurred","data":[{"line_number":2}]}',
164+
"partial write of line protocol occurred",
165+
),
166+
# data is not a dict when resolving fallback keys
167+
(
168+
"data not dict for fallback",
169+
'{"error":"data not list","data":"oops"}',
170+
"data not list",
171+
),
172+
]
173+
for name, response_body, expected in cases:
174+
with self.subTest(name):
175+
with self.assertRaises(InfluxDBError) as err:
176+
self._test_api_error(response_body)
177+
self.assertEqual(expected, err.exception.message)
178+
127179
def test_api_error_headers(self):
128180
body = '{"error": "test error"}'
129181
body_dic = json.loads(body)

tests/test_influxdb_client_3_integration.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
from influxdb_client_3 import InfluxDBClient3, write_client_options, WriteOptions, \
1414
WriteType, InfluxDB3ClientQueryError
15+
from influxdb_client_3.write_client.rest import ApiException
1516
from influxdb_client_3.exceptions import InfluxDBError
1617
from tests.util import asyncio_run, lp_to_py_object
1718

@@ -124,6 +125,39 @@ def test_write_and_query(self):
124125
self.assertEqual(test_id, df['test_id'][0])
125126
self.assertEqual(123.0, df['value'][0])
126127

128+
def test_v3_error(self):
129+
measurement = f'test{random_hex(3)}'.lower()
130+
lp = "\n".join([
131+
f"{measurement} v=1i 1770291280",
132+
f"{measurement} v=1 1770291281",
133+
])
134+
135+
with InfluxDBClient3(
136+
host=self.host,
137+
database=self.database,
138+
token=self.token,
139+
write_client_options=write_client_options(
140+
write_options=WriteOptions(
141+
write_type=WriteType.synchronous,
142+
no_sync=True
143+
)
144+
)
145+
) as client:
146+
try:
147+
client.write(lp)
148+
self.fail("Expected InfluxDBError from invalid line protocol.")
149+
except ApiException as err:
150+
if "Server doesn't support write with no_sync=true" in str(err):
151+
self.skipTest("no_sync not supported by this server.")
152+
msg = err.message
153+
self.assertIn("partial write of line protocol occurred", msg)
154+
self.assertIn((
155+
"invalid column type for column 'v', expected iox::column_type::field::integer, "
156+
"got iox::column_type::field::float"
157+
), msg)
158+
self.assertIn("line 2", msg)
159+
self.assertIn(measurement, msg)
160+
127161
def test_auth_error_token(self):
128162
self.client = InfluxDBClient3(host=self.host, database=self.database, token='fake token')
129163
test_id = time.time_ns()

0 commit comments

Comments
 (0)