Skip to content

Commit 56d9b6e

Browse files
committed
Stop hiding cause of last exception
1 parent 4717288 commit 56d9b6e

File tree

4 files changed

+172
-68
lines changed

4 files changed

+172
-68
lines changed

elastic_transport/_exceptions.py

-18
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,6 @@ def __repr__(self) -> str:
5050
parts.append(f"errors={self.errors!r}")
5151
return "{}({})".format(self.__class__.__name__, ", ".join(parts))
5252

53-
def __str__(self) -> str:
54-
return str(self.message)
55-
5653

5754
class SniffingError(TransportError):
5855
"""Error that occurs during the sniffing of nodes"""
@@ -67,29 +64,14 @@ class SerializationError(TransportError):
6764
class ConnectionError(TransportError):
6865
"""Error raised by the HTTP connection"""
6966

70-
def __str__(self) -> str:
71-
if self.errors:
72-
return f"Connection error caused by: {self.errors[0].__class__.__name__}({self.errors[0]})"
73-
return "Connection error"
74-
7567

7668
class TlsError(ConnectionError):
7769
"""Error raised by during the TLS handshake"""
7870

79-
def __str__(self) -> str:
80-
if self.errors:
81-
return f"TLS error caused by: {self.errors[0].__class__.__name__}({self.errors[0]})"
82-
return "TLS error"
83-
8471

8572
class ConnectionTimeout(TransportError):
8673
"""Connection timed out during an operation"""
8774

88-
def __str__(self) -> str:
89-
if self.errors:
90-
return f"Connection timeout caused by: {self.errors[0].__class__.__name__}({self.errors[0]})"
91-
return "Connection timed out"
92-
9375

9476
class ApiError(Exception):
9577
"""Base-class for clients that raise errors due to a response such as '404 Not Found'"""

tests/async_/test_async_transport.py

+87-13
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,14 @@
4545
from tests.conftest import AsyncDummyNode
4646

4747

48+
def exception_to_dict(exc: TransportError) -> dict:
49+
return {
50+
"type": exc.__class__.__name__,
51+
"message": exc.message,
52+
"errors": [exception_to_dict(e) for e in exc.errors],
53+
}
54+
55+
4856
@pytest.mark.asyncio
4957
async def test_async_transport_httpbin(httpbin_node_config):
5058
t = AsyncTransport([httpbin_node_config], meta_header=False)
@@ -139,14 +147,39 @@ async def test_request_will_fail_after_x_retries():
139147
)
140148
],
141149
node_class=AsyncDummyNode,
150+
max_retries=0,
142151
)
143152

144153
with pytest.raises(ConnectionError) as e:
145154
await t.perform_request("GET", "/")
146155

156+
assert exception_to_dict(e.value) == {
157+
"type": "ConnectionError",
158+
"message": "abandon ship",
159+
"errors": [],
160+
}
161+
162+
# max_retries=2
163+
with pytest.raises(ConnectionError) as e:
164+
await t.perform_request("GET", "/", max_retries=2)
165+
147166
assert 4 == len(t.node_pool.get().calls)
148-
assert len(e.value.errors) == 3
149-
assert all(isinstance(error, ConnectionError) for error in e.value.errors)
167+
assert exception_to_dict(e.value) == {
168+
"type": "ConnectionError",
169+
"message": "abandon ship",
170+
"errors": [
171+
{
172+
"type": "ConnectionError",
173+
"message": "abandon ship",
174+
"errors": [],
175+
},
176+
{
177+
"type": "ConnectionError",
178+
"message": "abandon ship",
179+
"errors": [],
180+
},
181+
],
182+
}
150183

151184

152185
@pytest.mark.parametrize("retry_on_timeout", [True, False])
@@ -174,15 +207,30 @@ async def test_retry_on_timeout(retry_on_timeout):
174207
)
175208

176209
if retry_on_timeout:
177-
with pytest.raises(ConnectionError) as e:
210+
with pytest.raises(TransportError) as e:
178211
await t.perform_request("GET", "/")
179-
assert len(e.value.errors) == 1
180-
assert isinstance(e.value.errors[0], ConnectionTimeout)
212+
213+
assert exception_to_dict(e.value) == {
214+
"type": "ConnectionError",
215+
"message": "error!",
216+
"errors": [
217+
{
218+
"type": "ConnectionTimeout",
219+
"message": "abandon ship",
220+
"errors": [],
221+
}
222+
],
223+
}
181224

182225
else:
183-
with pytest.raises(ConnectionTimeout) as e:
226+
with pytest.raises(TransportError) as e:
184227
await t.perform_request("GET", "/")
185-
assert len(e.value.errors) == 0
228+
229+
assert exception_to_dict(e.value) == {
230+
"type": "ConnectionTimeout",
231+
"message": "abandon ship",
232+
"errors": [],
233+
}
186234

187235

188236
@pytest.mark.asyncio
@@ -254,8 +302,27 @@ async def test_failed_connection_will_be_marked_as_dead():
254302
await t.perform_request("GET", "/")
255303
assert 0 == len(t.node_pool._alive_nodes)
256304
assert 2 == len(t.node_pool._dead_nodes.queue)
257-
assert len(e.value.errors) == 3
258-
assert all(isinstance(error, ConnectionError) for error in e.value.errors)
305+
assert exception_to_dict(e.value) == {
306+
"type": "ConnectionError",
307+
"message": "abandon ship",
308+
"errors": [
309+
{
310+
"type": "ConnectionError",
311+
"message": "abandon ship",
312+
"errors": [],
313+
},
314+
{
315+
"type": "ConnectionError",
316+
"message": "abandon ship",
317+
"errors": [],
318+
},
319+
{
320+
"type": "ConnectionError",
321+
"message": "abandon ship",
322+
"errors": [],
323+
},
324+
],
325+
}
259326

260327

261328
@pytest.mark.asyncio
@@ -602,7 +669,12 @@ def sniff_error(*_):
602669

603670
with pytest.raises(TransportError) as e:
604671
await t.perform_request("GET", "/")
605-
assert str(e.value) == "This is an error!"
672+
673+
assert exception_to_dict(e.value) == {
674+
"type": "TransportError",
675+
"message": "This is an error!",
676+
"errors": [],
677+
}
606678

607679
assert t._last_sniffed_at == last_sniffed_at
608680
assert t._sniffing_task.done()
@@ -628,9 +700,11 @@ async def test_sniff_on_start_no_results_errors(sniff_callback):
628700
with pytest.raises(SniffingError) as e:
629701
await t._async_call()
630702

631-
assert (
632-
str(e.value) == "No viable nodes were discovered on the initial sniff attempt"
633-
)
703+
assert exception_to_dict(e.value) == {
704+
"type": "SniffingError",
705+
"message": "No viable nodes were discovered on the initial sniff attempt",
706+
"errors": [],
707+
}
634708

635709

636710
@pytest.mark.parametrize("pool_size", [1, 8])

tests/conftest.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
# specific language governing permissions and limitations
1616
# under the License.
1717

18+
import copy
1819
import hashlib
1920
import logging
2021
import socket
@@ -40,7 +41,8 @@ def __init__(self, config: NodeConfig):
4041
def perform_request(self, *args, **kwargs):
4142
self.calls.append((args, kwargs))
4243
if self.exception:
43-
raise self.exception
44+
# Raising the same exception can cause recursion errors when exceptions are linked together
45+
raise copy.deepcopy(self.exception)
4446
meta = ApiResponseMeta(
4547
node=self.config,
4648
duration=0.0,
@@ -55,7 +57,8 @@ class AsyncDummyNode(DummyNode):
5557
async def perform_request(self, *args, **kwargs):
5658
self.calls.append((args, kwargs))
5759
if self.exception:
58-
raise self.exception
60+
# Raising the same exception can cause recursion errors when exceptions are linked together
61+
raise copy.deepcopy(self.exception)
5962
meta = ApiResponseMeta(
6063
node=self.config,
6164
duration=0.0,

tests/test_transport.py

+80-35
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,14 @@
4242
from tests.conftest import DummyNode
4343

4444

45+
def exception_to_dict(exc: TransportError) -> dict:
46+
return {
47+
"type": exc.__class__.__name__,
48+
"message": exc.message,
49+
"errors": [exception_to_dict(e) for e in exc.errors],
50+
}
51+
52+
4553
def test_transport_close_node_pool():
4654
t = Transport([NodeConfig("http", "localhost", 443)])
4755
with mock.patch.object(t.node_pool.all()[0], "close") as node_close:
@@ -138,37 +146,33 @@ def test_request_will_fail_after_x_retries():
138146
with pytest.raises(ConnectionError) as e:
139147
t.perform_request("GET", "/")
140148

141-
assert 1 == len(t.node_pool.get().calls)
142-
assert len(e.value.errors) == 0
143-
144-
# max_retries=3
145-
t = Transport(
146-
[
147-
NodeConfig(
148-
"http",
149-
"localhost",
150-
80,
151-
_extras={"exception": ConnectionError("abandon ship")},
152-
)
153-
],
154-
node_class=DummyNode,
155-
max_retries=3,
156-
)
157-
158-
with pytest.raises(ConnectionError) as e:
159-
t.perform_request("GET", "/")
160-
161-
assert 4 == len(t.node_pool.get().calls)
162-
assert len(e.value.errors) == 3
163-
assert all(isinstance(error, ConnectionError) for error in e.value.errors)
149+
assert exception_to_dict(e.value) == {
150+
"type": "ConnectionError",
151+
"message": "abandon ship",
152+
"errors": [],
153+
}
164154

165155
# max_retries=2 in perform_request()
166156
with pytest.raises(ConnectionError) as e:
167157
t.perform_request("GET", "/", max_retries=2)
168158

169-
assert 7 == len(t.node_pool.get().calls)
170-
assert len(e.value.errors) == 2
171-
assert all(isinstance(error, ConnectionError) for error in e.value.errors)
159+
assert 4 == len(t.node_pool.get().calls)
160+
assert exception_to_dict(e.value) == {
161+
"type": "ConnectionError",
162+
"message": "abandon ship",
163+
"errors": [
164+
{
165+
"type": "ConnectionError",
166+
"message": "abandon ship",
167+
"errors": [],
168+
},
169+
{
170+
"type": "ConnectionError",
171+
"message": "abandon ship",
172+
"errors": [],
173+
},
174+
],
175+
}
172176

173177

174178
@pytest.mark.parametrize("retry_on_timeout", [True, False])
@@ -197,13 +201,28 @@ def test_retry_on_timeout(retry_on_timeout):
197201
if retry_on_timeout:
198202
with pytest.raises(ConnectionError) as e:
199203
t.perform_request("GET", "/")
200-
assert len(e.value.errors) == 1
201-
assert isinstance(e.value.errors[0], ConnectionTimeout)
204+
205+
assert exception_to_dict(e.value) == {
206+
"type": "ConnectionError",
207+
"message": "error!",
208+
"errors": [
209+
{
210+
"type": "ConnectionTimeout",
211+
"message": "abandon ship",
212+
"errors": [],
213+
}
214+
],
215+
}
202216

203217
else:
204218
with pytest.raises(ConnectionTimeout) as e:
205219
t.perform_request("GET", "/")
206-
assert len(e.value.errors) == 0
220+
221+
assert exception_to_dict(e.value) == {
222+
"type": "ConnectionTimeout",
223+
"message": "abandon ship",
224+
"errors": [],
225+
}
207226

208227

209228
def test_retry_on_status():
@@ -273,8 +292,27 @@ def test_failed_connection_will_be_marked_as_dead():
273292
t.perform_request("GET", "/")
274293
assert 0 == len(t.node_pool._alive_nodes)
275294
assert 2 == len(t.node_pool._dead_nodes.queue)
276-
assert len(e.value.errors) == 3
277-
assert all(isinstance(error, ConnectionError) for error in e.value.errors)
295+
assert exception_to_dict(e.value) == {
296+
"type": "ConnectionError",
297+
"message": "abandon ship",
298+
"errors": [
299+
{
300+
"type": "ConnectionError",
301+
"message": "abandon ship",
302+
"errors": [],
303+
},
304+
{
305+
"type": "ConnectionError",
306+
"message": "abandon ship",
307+
"errors": [],
308+
},
309+
{
310+
"type": "ConnectionError",
311+
"message": "abandon ship",
312+
"errors": [],
313+
},
314+
],
315+
}
278316

279317

280318
def test_resurrected_connection_will_be_marked_as_live_on_success():
@@ -603,7 +641,12 @@ def sniff_error(*_):
603641

604642
with pytest.raises(TransportError) as e:
605643
t.perform_request("GET", "/")
606-
assert str(e.value) == "This is an error!"
644+
645+
assert exception_to_dict(e.value) == {
646+
"type": "TransportError",
647+
"message": "This is an error!",
648+
"errors": [],
649+
}
607650

608651
assert t._last_sniffed_at == last_sniffed_at
609652
assert t._sniffing_lock.locked() is False
@@ -620,9 +663,11 @@ def test_sniff_on_start_no_results_errors():
620663
sniff_callback=lambda *_: [],
621664
)
622665

623-
assert (
624-
str(e.value) == "No viable nodes were discovered on the initial sniff attempt"
625-
)
666+
assert exception_to_dict(e.value) == {
667+
"type": "SniffingError",
668+
"message": "No viable nodes were discovered on the initial sniff attempt",
669+
"errors": [],
670+
}
626671

627672

628673
@pytest.mark.parametrize("pool_size", [1, 8])

0 commit comments

Comments
 (0)