Skip to content

fix(routing): treat a profile with no rules like one with none active - #388

Merged
prajjwalkumar17 merged 2 commits into
mainfrom
fix/zero-rule-fallback
Aug 31, 2026
Merged

fix(routing): treat a profile with no rules like one with none active#388
prajjwalkumar17 merged 2 commits into
mainfrom
fix/zero-rule-fallback

Conversation

@prajjwalkumar17

@prajjwalkumar17 prajjwalkumar17 commented Aug 30, 2026

Copy link
Copy Markdown
Member

Description

routing_evaluate answered ActiveRoutingAlgorithmNotFound with a 200 and 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 hard 400 instead — ~1,664/day, all shadow-path, from ~30 cyMerchant_* Cypress profiles.

To the caller those two states are the same: Hyperswitch routes by the fallback it supplied either way, so the 400 only ever produced a failure it could not act on. Both now take the same 200.

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_response defaults the output to empty, so a 200 would read as a successful routing decision to nowhere. That stays an error, and routing-hybrid.spec.ts:71 already enforced it.

This also drops profile_has_any_routing_rule and the per-request SELECT it 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.

⚠️ This changes a documented contract

routing-rule-mutations.spec.ts previously asserted:

evaluating for a merchant with no rules at all still errors"the engine was never given a rule for this profile, so it has nothing to say and the caller should fall back to its own configuration. That stays a 400."

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 400 was 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_algorithm raises ActiveRoutingAlgorithmNotFound only for MeshError::NotFound. Every other storage error becomes EuclidErrors::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 — clean
  • routing-rule-mutations.spec.ts + routing-hybrid.spec.ts run locally against a live engine: 13 passed, including the unchanged no-fallback guard

🤖 Generated with Claude Code

Closes #400

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>
Copilot AI lite review requested due to automatic review settings August 30, 2026 16:45

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@prajjwalkumar17 prajjwalkumar17 self-assigned this Aug 30, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Evaluate answers a hard 400 for profiles the engine was never given a rule for

2 participants