retry_if_exception_message: use re.search for the match= pattern so it finds the regex anywhere in the message - #652
Conversation
…t finds the regex anywhere in the exception message, not only at the start
| retry=tenacity.retry_if_exception_message( | ||
| message=NoCustomErrorAfterCount.derived_message | ||
| ), | ||
| stop=tenacity.stop_after_attempt(5), |
Mukller
left a comment
There was a problem hiding this comment.
Verified locally against upstream master and this branch (Python 3.13):
The fix works and is well-targeted:
retry_if_exception_message(match="limit exceeded")
message = "HTTP 429: rate limit exceeded"
master (re.match): _check -> False <- zero retries, the bug
branch (re.search): _check -> Truetests/test_tenacity.py -k exception_message — 8/8 green on the branch, including the new regression test with a realistic message shape.
Compatibility surface, checked explicitly: anchored patterns behave identically (^HTTP 429 matches at offset 0 and rejects mid-message occurrences under both match() and search()), so users who consciously anchored are safe. The flip affects unanchored patterns that previously failed everywhere except position 0 — e.g. match="429" now matches "[OK] 429 done" where master returned False. For most users that's the semantics they intended all along (and what the parameter name suggests), but it can only be discovered as a sudden change in retry behavior.
Given that, two suggestions:
- Changelog entry should call this out as a behavior change, not just a fix — anyone auditing retries after upgrading will want this line.
- If maintainers are worried about the flip, an opt-in flag (
search=True) would preserve old semantics; personally I think search-as-default matches user intent better and a major-version note suffices.
One tiny nit: the inline comment explains re.search vs re.match, but the same reasoning applies to the message= equality branch above it — no action needed, just noting the comment reads slightly like it covers _check as a whole.
Bug
retry_if_exception_message(match=...)compiled the user's regex and calledre.Pattern.match(), which only matches at the start of the string. The parameter is namedmatchand the docstring just says "matches" — so users reasonably expect the pattern to be found anywhere in the exception message, the same wayre.searchdoes. With the current implementation, a user who wrote:got a retry on
ValueError("rate limit hit")(works by accident) but no retry onValueError("HTTP 429: rate limit hit")(pattern not at the start). That second case is the common real-world one and is exactly the kind of message HTTP libraries produce.Fix
Use
self.match.search(...)instead ofself.match.match(...)inretry_if_exception_message._check. The behaviour ofmessage=(full-string equality) is unchanged.Test
Added
test_retry_if_exception_message_match_finds_pattern_anywhereplus a module-level helper that raisesCustomError("HTTP 429: rate limit exceeded")and matches on"limit exceeded"(which does not occur at position 0). The pre-existing testtest_retry_if_exception_message_matchstill passes — it uses a pattern prefixed withderived_message[:3] + ".*"so bothre.matchandre.searchaccept it.Full suite: 128 passed.