Skip to content

Commit 940eac2

Browse files
committed
fix: retry 5xx responses without a dedicated exception on another node (#145)
1 parent ef3a3db commit 940eac2

2 files changed

Lines changed: 38 additions & 1 deletion

File tree

‎src/typesense/request_handler.py‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -309,10 +309,15 @@ def _get_exception(http_code: int) -> typing.Type[TypesenseClientError]:
309309
"""
310310
Map an HTTP status code to the appropriate exception type.
311311
312+
Any 5xx code without a dedicated exception (e.g. 502, 504 from a proxy
313+
in front of a restarting node) maps to ServerError, so the request is
314+
retried on another node.
315+
312316
Args:
313317
http_code (int): The HTTP status code.
314318
315319
Returns:
316320
Type[TypesenseClientError]: The exception type corresponding to the status code.
317321
"""
318-
return _ERROR_CODE_MAP.get(str(http_code), TypesenseClientError)
322+
default = ServerError if 500 <= http_code <= 599 else TypesenseClientError
323+
return _ERROR_CODE_MAP.get(str(http_code), default)

‎tests/api_call_test.py‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,11 @@ def test_get_exception() -> None:
8888
assert RequestHandler._get_exception(422) == exceptions.ObjectUnprocessable
8989
assert RequestHandler._get_exception(500) == exceptions.ServerError
9090
assert RequestHandler._get_exception(503) == exceptions.ServiceUnavailable
91+
assert RequestHandler._get_exception(501) == exceptions.ServerError
92+
assert RequestHandler._get_exception(502) == exceptions.ServerError
93+
assert RequestHandler._get_exception(504) == exceptions.ServerError
94+
assert RequestHandler._get_exception(599) == exceptions.ServerError
95+
assert RequestHandler._get_exception(418) == exceptions.TypesenseClientError
9196
assert RequestHandler._get_exception(999) == exceptions.TypesenseClientError
9297

9398

@@ -460,6 +465,33 @@ def test_selects_next_available_node_on_timeout(
460465
assert len(respx.calls) == 3
461466

462467

468+
@pytest.mark.parametrize("status_code", [500, 502, 503, 504])
469+
def test_selects_next_available_node_on_5xx(
470+
fake_api_call: ApiCall,
471+
status_code: int,
472+
) -> None:
473+
"""Test that a 5xx response marks the node unhealthy and retries on the next one."""
474+
with respx.mock:
475+
respx.get("http://nearest:8108/test").mock(
476+
return_value=httpx.Response(status_code, text="Bad Gateway")
477+
)
478+
respx.get("http://node0:8108/test").mock(
479+
return_value=httpx.Response(200, json={"key": "value"})
480+
)
481+
482+
response = fake_api_call.get(
483+
"/test",
484+
as_json=True,
485+
entity_type=typing.Dict[str, str],
486+
)
487+
488+
assert response == {"key": "value"}
489+
assert respx.calls[0].request.url == "http://nearest:8108/test"
490+
assert respx.calls[1].request.url == "http://node0:8108/test"
491+
assert len(respx.calls) == 2
492+
assert fake_api_call.config.nearest_node.healthy is False
493+
494+
463495
def test_get_node_no_healthy_nodes(
464496
fake_api_call: ApiCall,
465497
mocker: MockFixture,

0 commit comments

Comments
 (0)