diff --git a/tenacity/retry.py b/tenacity/retry.py index df0cc4d6..e4b5a274 100644 --- a/tenacity/retry.py +++ b/tenacity/retry.py @@ -241,7 +241,12 @@ def _check(self, exception: BaseException) -> bool: if self.message: return self.message == str(exception) assert self.match is not None - return bool(self.match.match(str(exception))) + # Use re.search (not re.match) so the pattern is found anywhere in + # the exception message. re.match only matches at the start, so a + # user-supplied pattern like r"rate limit" would never match + # "HTTP 429: rate limit hit". The parameter is named ``match``, + # meaning "find a match in the message", not "match from offset 0". + return bool(self.match.search(str(exception))) class retry_if_not_exception_message(retry_if_exception_message): diff --git a/tests/test_tenacity.py b/tests/test_tenacity.py index 9ecdea56..82540717 100644 --- a/tests/test_tenacity.py +++ b/tests/test_tenacity.py @@ -1173,10 +1173,10 @@ def _retryable_test_with_unless_exception_type_no_input( @retry( - stop=tenacity.stop_after_attempt(5), retry=tenacity.retry_if_exception_message( message=NoCustomErrorAfterCount.derived_message ), + stop=tenacity.stop_after_attempt(5), ) def _retryable_test_if_exception_message_message(thing: typing.Any) -> typing.Any: return thing.go() @@ -1209,6 +1209,14 @@ def _retryable_test_if_not_exception_message_match(thing: typing.Any) -> typing. return thing.go() +@retry( + retry=tenacity.retry_if_exception_message(match="limit exceeded"), + stop=tenacity.stop_after_attempt(5), +) +def _retryable_test_if_exception_message_match_search(thing: typing.Any) -> typing.Any: + return thing.go() + + @retry( retry=tenacity.retry_if_not_exception_message( message=NameErrorUntilCount.derived_message @@ -1378,6 +1386,36 @@ def test_retry_if_exception_message_match(self) -> None: except CustomError: self.fail("CustomError should've been retried from errormessage") + def test_retry_if_exception_message_match_finds_pattern_anywhere(self) -> None: + # Regression: the `match` kwarg used to call re.Pattern.match(), which + # only matches at the start of the message. A user who wrote + # `match="limit exceeded"` to catch `"HTTP 429: rate limit exceeded"` + # got zero retries because the pattern was not anchored at index 0. + # The fix uses re.Pattern.search() so the pattern is found anywhere + # in the message, which is what the parameter name implies. + class MessageCount: + derived_message = "HTTP 429: rate limit exceeded" + + def __init__(self, count: int) -> None: + self.counter = 0 + self.count = count + + def go(self) -> bool: + if self.counter < self.count: + self.counter += 1 + raise CustomError(self.derived_message) + return True + + try: + self.assertTrue( + _retryable_test_if_exception_message_match_search(MessageCount(3)) + ) + except CustomError: + self.fail( + "CustomError should've been retried: the 'limit exceeded' pattern " + "appears in the message and should trigger a retry" + ) + def test_retry_if_not_exception_message_match(self) -> None: try: self.assertTrue(