441. Default behavior: RestTransportError is NOT retried (even for GET)
552. Opt-in behavior: RestTransportError retried for configured methods only
663. Existing HTTP error retry behavior unchanged
7+ 4. Method extraction from exception reason strings
78"""
89
910import pytest
1718 RestTransportError ,
1819 ServiceException ,
1920)
20- from hatchet_sdk .clients .rest .tenacity_utils import tenacity_should_retry
21+ from hatchet_sdk .clients .rest .tenacity_utils import (
22+ _extract_method_from_reason ,
23+ tenacity_should_retry ,
24+ )
2125from hatchet_sdk .config import TenacityConfig
2226
2327# --- Default behavior tests (transport errors NOT retried) ---
2428
2529
26- def test_default__rest_transport_error_not_retried () -> None :
27- """By default, RestTransportError should NOT be retried."""
28- exc = RestTransportError (status = 0 , reason = "method=GET\n url=http://test" )
29- config = TenacityConfig ()
30- assert tenacity_should_retry (exc , config ) is False
31-
32-
33- def test_default__rest_timeout_error_not_retried () -> None :
34- """By default, RestTimeoutError should NOT be retried."""
35- exc = RestTimeoutError (status = 0 , reason = "method=GET\n url=http://test" )
36- config = TenacityConfig ()
37- assert tenacity_should_retry (exc , config ) is False
38-
39-
40- def test_default__rest_connection_error_not_retried () -> None :
41- """By default, RestConnectionError should NOT be retried."""
42- exc = RestConnectionError (status = 0 , reason = "method=GET\n url=http://test" )
43- config = TenacityConfig ()
44- assert tenacity_should_retry (exc , config ) is False
45-
46-
47- def test_default__rest_tls_error_not_retried () -> None :
48- """By default, RestTLSError should NOT be retried."""
49- exc = RestTLSError (status = 0 , reason = "method=GET\n url=http://test" )
50- config = TenacityConfig ()
51- assert tenacity_should_retry (exc , config ) is False
52-
53-
54- def test_default__rest_protocol_error_not_retried () -> None :
55- """By default, RestProtocolError should NOT be retried."""
56- exc = RestProtocolError (status = 0 , reason = "method=GET\n url=http://test" )
30+ @pytest .mark .parametrize (
31+ "exc_class" ,
32+ [RestTransportError , RestTimeoutError ],
33+ ids = ["base-class" , "subclass" ],
34+ )
35+ def test_default__transport_errors_not_retried (exc_class : type ) -> None :
36+ """By default, RestTransportError and subclasses should not be retried."""
37+ exc = exc_class (status = 0 , reason = "method=GET\n url=http://test" )
5738 config = TenacityConfig ()
5839 assert tenacity_should_retry (exc , config ) is False
5940
6041
6142# --- Opt-in behavior tests (transport errors retried for allowed methods) ---
6243
6344
64- def test_optin__get_request_retried () -> None :
65- """When enabled, GET requests with transport errors should be retried."""
66- exc = RestTimeoutError (status = 0 , reason = "method=GET\n url=http://test" )
67- config = TenacityConfig (retry_transport_errors = True )
68- assert tenacity_should_retry (exc , config ) is True
69-
70-
71- def test_optin__delete_request_retried () -> None :
72- """When enabled, DELETE requests with transport errors should be retried."""
73- exc = RestConnectionError (status = 0 , reason = "method=DELETE\n url=http://test" )
45+ @pytest .mark .parametrize (
46+ "method" ,
47+ ["GET" , "DELETE" ],
48+ ids = ["get" , "delete" ],
49+ )
50+ def test_optin__idempotent_methods_retried (method : str ) -> None :
51+ """When enabled, GET and DELETE requests with transport errors should be retried."""
52+ exc = RestTimeoutError (status = 0 , reason = f"method={ method } \n url=http://test" )
7453 config = TenacityConfig (retry_transport_errors = True )
7554 assert tenacity_should_retry (exc , config ) is True
7655
7756
78- def test_optin__post_request_not_retried () -> None :
79- """POST requests should NOT be retried even when transport retry is enabled."""
80- exc = RestTimeoutError (status = 0 , reason = "method=POST\n url=http://test" )
81- config = TenacityConfig (retry_transport_errors = True )
82- assert tenacity_should_retry (exc , config ) is False
83-
84-
85- def test_optin__put_request_not_retried () -> None :
86- """PUT requests should NOT be retried even when transport retry is enabled."""
87- exc = RestConnectionError (status = 0 , reason = "method=PUT\n url=http://test" )
88- config = TenacityConfig (retry_transport_errors = True )
89- assert tenacity_should_retry (exc , config ) is False
90-
91-
92- def test_optin__patch_request_not_retried () -> None :
93- """PATCH requests should NOT be retried even when transport retry is enabled."""
94- exc = RestProtocolError (status = 0 , reason = "method=PATCH\n url=http://test" )
57+ @pytest .mark .parametrize (
58+ "method" ,
59+ ["POST" , "PUT" , "PATCH" ],
60+ ids = ["post" , "put" , "patch" ],
61+ )
62+ def test_optin__non_idempotent_methods_not_retried (method : str ) -> None :
63+ """Non-idempotent requests should not be retried even when transport retry is enabled."""
64+ exc = RestTimeoutError (status = 0 , reason = f"method={ method } \n url=http://test" )
9565 config = TenacityConfig (retry_transport_errors = True )
9666 assert tenacity_should_retry (exc , config ) is False
9767
@@ -101,65 +71,88 @@ def test_optin__custom_methods_list() -> None:
10171 exc = RestTimeoutError (status = 0 , reason = "method=POST\n url=http://test" )
10272 config = TenacityConfig (
10373 retry_transport_errors = True ,
104- retry_transport_methods = ["POST" ], # allow POST explicitly
74+ retry_transport_methods = ["POST" ],
10575 )
10676 assert tenacity_should_retry (exc , config ) is True
10777
10878
109- def test_optin__custom_methods_excludes_get () -> None :
110- """Custom retry_transport_methods can exclude GET."""
79+ def test_optin__custom_methods_excludes_default () -> None :
80+ """Custom retry_transport_methods can exclude default methods like GET."""
11181 exc = RestTimeoutError (status = 0 , reason = "method=GET\n url=http://test" )
11282 config = TenacityConfig (
11383 retry_transport_errors = True ,
114- retry_transport_methods = ["DELETE" ], # only DELETE, not GET
84+ retry_transport_methods = ["DELETE" ],
11585 )
11686 assert tenacity_should_retry (exc , config ) is False
11787
11888
11989# --- Regression tests: existing HTTP error retry behavior unchanged ---
12090
12191
122- def test_regression__service_exception_still_retried () -> None :
123- """ServiceException (5xx) should still be retried."""
124- exc = ServiceException ( status = 500 , reason = "Internal Server Error" )
125- config = TenacityConfig ()
126- assert tenacity_should_retry ( exc , config ) is True
127-
128-
129- def test_regression__not_found_exception_still_retried () -> None :
130- """NotFoundException (404) should still be retried."""
131- exc = NotFoundException ( status = 404 , reason = "Not Found" )
92+ @ pytest . mark . parametrize (
93+ ( "exc" , "desc" ),
94+ [
95+ ( ServiceException ( status = 500 , reason = "Internal Server Error" ), "5xx" ),
96+ ( NotFoundException ( status = 404 , reason = "Not Found" ), "404" ),
97+ ],
98+ ids = [ "service-exception" , "not-found" ],
99+ )
100+ def test_regression__http_errors_still_retried ( exc : Exception , desc : str ) -> None :
101+ """ServiceException (5xx) and NotFoundException ( 404) should still be retried."""
132102 config = TenacityConfig ()
133103 assert tenacity_should_retry (exc , config ) is True
134104
135105
136- def test_regression__service_exception_retried_without_config () -> None :
106+ def test_regression__backward_compat_no_config () -> None :
137107 """ServiceException should be retried even without config (backward compat)."""
138108 exc = ServiceException (status = 500 , reason = "Internal Server Error" )
139- # Call without config to test backward compatibility
140109 assert tenacity_should_retry (exc ) is True
141110
142111
143- # --- Edge cases ---
144-
145-
146- def test_edge__missing_method_in_reason_not_retried () -> None :
147- """If method cannot be extracted from reason, should not retry."""
148- exc = RestTimeoutError (status = 0 , reason = "some error without method" )
149- config = TenacityConfig (retry_transport_errors = True )
150- assert tenacity_should_retry (exc , config ) is False
112+ # --- Unit tests for _extract_method_from_reason ---
113+
114+
115+ @pytest .mark .parametrize (
116+ ("reason" , "expected" ),
117+ [
118+ ("method=GET\n url=http://test" , "GET" ),
119+ ("method=POST\n url=http://test" , "POST" ),
120+ ("method=delete\n url=http://test" , "delete" ),
121+ ("prefix method=PUT suffix" , "PUT" ),
122+ ("some error without method" , None ),
123+ ("method=\n url=http://test" , None ),
124+ ("" , None ),
125+ (None , None ),
126+ ],
127+ ids = [
128+ "get-uppercase" ,
129+ "post-uppercase" ,
130+ "lowercase-preserved" ,
131+ "embedded-in-text" ,
132+ "no-method-field" ,
133+ "empty-method-value" ,
134+ "empty-string" ,
135+ "none" ,
136+ ],
137+ )
138+ def test_extract_method__parses_reason (
139+ reason : str | None , expected : str | None
140+ ) -> None :
141+ """_extract_method_from_reason should correctly parse HTTP method from reason."""
142+ assert _extract_method_from_reason (reason ) == expected
151143
152144
153- def test_edge__empty_reason_not_retried () -> None :
154- """If reason is empty, should not retry."""
155- exc = RestConnectionError (status = 0 , reason = "" )
156- config = TenacityConfig (retry_transport_errors = True )
157- assert tenacity_should_retry (exc , config ) is False
145+ # --- Edge cases for retry behavior ---
158146
159147
160- def test_edge__none_reason_not_retried () -> None :
161- """If reason is None, should not retry."""
162- exc = RestTLSError (status = 0 , reason = None )
148+ @pytest .mark .parametrize (
149+ "reason" ,
150+ ["some error without method" , "" , None ],
151+ ids = ["no-method-field" , "empty-string" , "none" ],
152+ )
153+ def test_edge__unparseable_reason_not_retried (reason : str | None ) -> None :
154+ """If method cannot be extracted from reason, should not retry."""
155+ exc = RestTimeoutError (status = 0 , reason = reason )
163156 config = TenacityConfig (retry_transport_errors = True )
164157 assert tenacity_should_retry (exc , config ) is False
165158
0 commit comments