fix(routing): treat a profile with no rules like one with none active - #388
Merged
Conversation
routing_evaluate answered ActiveRoutingAlgorithmNotFound as a success only when the profile already had at least one rule, on the reasoning that a deactivated profile is stating a choice while a profile the engine has never been given any rules for is a different situation. That distinction does not survive contact with the caller: Hyperswitch routes by the fallback it supplied either way, so the second case only ever produced a failure the caller could not act on. Both now take the same 200 with the caller's fallback, which makes the engine correct for any caller that reaches it without an active rule rather than only for the ones Hyperswitch happens to filter first. It also removes the extra per-request lookup profile_has_any_routing_rule needed, which ran on every evaluate a ruleless profile made -- exactly the profiles least able to justify the query. A profile still awaiting migration is reported by the caller, which warns when a cut-over profile gets an empty result. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The first cut answered every ActiveRoutingAlgorithmNotFound with a 200, which dropped a guard worth keeping: with no rule and no fallback there is nothing to answer with, and no_active_algorithm_response defaults the output to empty, so the caller reads a success that carries nowhere to route. Require a non-empty fallback_output before taking the graceful path. A profile with no rules but a fallback now gets the same answer a deactivated profile already got; one with neither still gets the error it needs. This changes a documented contract: routing-rule-mutations asserted that a merchant with no rules at all errors even when a fallback was supplied. That is precisely the case the shadow traffic fails on, so the test now asserts the fallback answer, and a new case covers the no-fallback guard that the hybrid spec was already enforcing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
routing_evaluateansweredActiveRoutingAlgorithmNotFoundwith a200and the caller's fallback only when the profile already had at least one routing rule. A profile the engine had never been given a rule for got a hard400instead — ~1,664/day, all shadow-path, from ~30cyMerchant_*Cypress profiles.To the caller those two states are the same: Hyperswitch routes by the fallback it supplied either way, so the
400only ever produced a failure it could not act on. Both now take the same200.The condition is "did the caller supply a fallback", not "does the profile have rules"
The graceful path is taken when the caller supplied a non-empty
fallback_output. With no rule and no fallback there is nothing to answer with —no_active_algorithm_responsedefaults the output to empty, so a200would read as a successful routing decision to nowhere. That stays an error, androuting-hybrid.spec.ts:71already enforced it.This also drops
profile_has_any_routing_ruleand the per-requestSELECTit performed. That query had neither a cache entry nor an active mapper row to short-circuit it, so it ran on every evaluate from a profile with no active rule — exactly the profiles least able to justify an extra round trip.routing-rule-mutations.spec.tspreviously asserted:That is the exact behaviour the shadow traffic fails on, so the test now asserts the fallback answer instead, and a new case covers the no-fallback guard. Flagging explicitly for whoever wrote that spec — if the
400was load-bearing for a caller I haven't found, say so and I'll close this in favour of the Hyperswitch-side fix alone.Why it stays safe
resolve_active_algorithmraisesActiveRoutingAlgorithmNotFoundonly forMeshError::NotFound. Every other storage error becomesEuclidErrors::StorageError, so an unreachable database still surfaces as a failure and never as a silent fallback.Companion change
Hyperswitch-side, shadow evaluation now requires an active routing algorithm, so these calls stop being made at all for such profiles. This PR is the backstop that makes the engine answer correctly for any caller that still reaches it.
Verification
cargo check/cargo clippy/cargo fmt --check— cleanrouting-rule-mutations.spec.ts+routing-hybrid.spec.tsrun locally against a live engine: 13 passed, including the unchanged no-fallback guard🤖 Generated with Claude Code
Closes #400