fix: don't reject queries using variables when cost_validator has none (#1319)#1325
fix: don't reject queries using variables when cost_validator has none (#1319)#1325jbbqqf wants to merge 1 commit into
Conversation
…ariables `cost_validator()` returns a validation rule that takes an optional `variables` dict, baked into the class at construction time. When the rule is added to a static `validation_rules=[cost_validator(...)]` list (the natural form), it never receives per-request variables, so `get_argument_values` raises a GraphQLError for any argument supplied via a query variable and the query is rejected with a misleading "was not provided a runtime value" message. When `self.variables is None`, treat the argument-resolution error as a cost-computation limitation rather than a user-facing validation error: fall back to an empty args dict so the query runs. Variable-driven multipliers are simply not counted in the cost. When the caller does pass an explicit `variables` dict, the prior behaviour is preserved — missing entries are still reported as validation errors. Fixes mirumee#1319.
📝 WalkthroughWalkthroughThe PR fixes a regression where ChangesVariable-aware exception handling in cost validation
Estimated code review effort🎯 2 (Simple) | ⏱️ ~8 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #1325 +/- ##
==========================================
+ Coverage 97.24% 97.30% +0.06%
==========================================
Files 127 127
Lines 9214 9241 +27
==========================================
+ Hits 8960 8992 +32
+ Misses 254 249 -5 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
🧹 Nitpick comments (1)
tests/test_query_cost_validation.py (1)
656-744: ⚡ Quick winAdd return type hints to the new test functions.
The new test defs at Line 656, Line 680, Line 696, and Line 725 should include explicit
-> Noneto match repo typing rules for Python files.Suggested diff
def test_query_with_required_variable_is_not_rejected_when_variables_not_passed( schema, -): +) -> None: @@ def test_query_with_required_input_variable_is_not_rejected_when_variables_not_passed( schema, -): +) -> None: @@ def test_cost_computation_skips_variable_multipliers_when_variables_not_passed( schema, -): +) -> None: @@ -def test_explicit_variables_dict_still_reports_missing_variable_errors(schema): +def test_explicit_variables_dict_still_reports_missing_variable_errors( + schema, +) -> None:As per coding guidelines: "Use Python 3.10+ with type hints throughout".
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/test_query_cost_validation.py` around lines 656 - 744, Add explicit return type hints "-> None" to the four new test functions so they conform to repo typing rules: update the definitions of test_query_with_required_variable_is_not_rejected_when_variables_not_passed, test_query_with_required_input_variable_is_not_rejected_when_variables_not_passed, test_cost_computation_skips_variable_multipliers_when_variables_not_passed, and test_explicit_variables_dict_still_reports_missing_variable_errors to include "-> None" before the colon.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@tests/test_query_cost_validation.py`:
- Around line 656-744: Add explicit return type hints "-> None" to the four new
test functions so they conform to repo typing rules: update the definitions of
test_query_with_required_variable_is_not_rejected_when_variables_not_passed,
test_query_with_required_input_variable_is_not_rejected_when_variables_not_passed,
test_cost_computation_skips_variable_multipliers_when_variables_not_passed, and
test_explicit_variables_dict_still_reports_missing_variable_errors to include
"-> None" before the colon.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 0df11db9-ff94-412c-bca4-7cce515cf5ef
📒 Files selected for processing (2)
ariadne/validation/query_cost.pytests/test_query_cost_validation.py
Decision review: Cost validator silently accepts queries when variable costs cannot be resolvedProduct decisions in this change
Assessment1. Allowing queries through when variable costs cannot be resolved This is the core decision and the most consequential one. The cost validator exists as a protection mechanism — to prevent users from sending queries that would be too expensive to execute. Silently allowing queries through when cost cannot be fully computed means the protection can be bypassed by any attacker who routes cost-multiplying arguments through query variables rather than inline literals. A query like The current behavior (rejecting queries) is clearly wrong — it breaks APIs that are misconfigured in a completely innocent way. But the proposed behavior (silently passing queries with incomplete cost estimates) is "quietly wrong" in a way that feels safe to a developer while offering reduced protection. The third option — making the degradation visible — is absent from this PR.
The silent fallback is better than spurious rejection for developers who never intended to use variable-driven cost multipliers (i.e., most users). But for developers who specifically want to cap query cost by limiting variable values, this fix removes protection while looking like it resolves the problem. Agree with direction, disagree with the absence of any developer signal. 2. This distinction is subtle and creates a trap. A developer who reads that the default is "no variables" and then decides to be explicit — writing The distinction does have a reasonable rationale (if you pass a dict, you're asserting it represents the available variables), but it is a footgun that the PR does not address. Disagree with leaving this undocumented. The API contract should be visible at the point of configuration, not discoverable only through failure. 3. No signal when cost protection is incomplete This is the decision the PR most clearly does not make — it is an absence of a decision. When variable-driven cost multipliers are skipped, the developer receives no indication. Queries pass, costs are computed, extensions report a cost number. Everything looks correct. For a security feature, silent degradation is a meaningful product decision: it means developers who audit their configuration (by looking at query results) will see costs being reported and assume the numbers are complete. A log-level warning, a note in the extensions payload (e.g., Disagree with silent degradation for a security mechanism. A visible signal should accompany the fallback. 4. The static rule form is now implicitly a best-effort tool This is consistent with prior art: many GraphQL cost-validation libraries distinguish between "static analysis" (approximate, no runtime values) and "runtime validation" (exact, with variables). Making the static form approximate is the right product direction. The problem is that this distinction is not surfaced anywhere a developer would find it — the API signature, the documentation, or the result extensions. Agree with the direction; the framing needs to be explicit in documentation. Open questions
RecommendationShip with changes The fix is correct in direction — spurious query rejection is worse than allowing queries through — but shipping a security feature that silently under-counts without any developer signal is a meaningful product risk. Before merging, add at minimum: (a) a developer-facing warning or extensions flag when variable costs are skipped, and (b) documentation that names the two modes (static/approximate vs. per-request/exact) and explains when each is appropriate. Without these, developers will configure the static form expecting full protection and have no way to discover the gap. |
There was a problem hiding this comment.
Caution
The cost-validator is a security control (DoS / query-complexity limiting). Silently falling back to field_args = {} when self.variables is None means any variable-driven multiplier is simply not...
ariadne/validation/query_cost.py:107
1 finding(s) posted as inline comments.
Warning
docs/06-Extensions/03-query-validators.md (line 131) [doc_conflict]: The section "Exposing query variables to cost_validator" (lines 129–152) now describes deprecated behaviour as current: the stated invariant ("will raise an error") is no longer true, and the callable-workaround example exists solely to avoid that error. After this PR the section is misleading and the recommended workaround is no longer necessary for the common case — the docs must be updated to reflect the new semantics and any remaining reason to prefer the callable form (accurate cost accounting when multipliers are variable-driven).
| # is allowed to run; the cost contribution from variable | ||
| # multipliers is simply not counted. When variables are | ||
| # supplied, real argument-resolution errors are still | ||
| # reported as before. See issue #1319. | ||
| if self.variables is None: |
There was a problem hiding this comment.
Caution
The cost-validator is a security control (DoS / query-complexity limiting). Silently falling back to field_args = {} when self.variables is None means any variable-driven multiplier is simply not counted — a client can send $count=10000 and the validator computes cost as if the argument were absent. The documented design (see docs/06-Extensions/03-query-validators.md lines 131–152) deliberately surfaces this as an error and provides a callable pattern so callers explicitly forward per-request variables; degrading silently instead removes the incentive to apply that pattern and allows variable-based cost inflation to bypass the limit undetected.
Summary
cost_validator()returns a validation rule that takes an optionalvariablesdict, baked into the class at construction time. When the rule is added to a staticvalidation_rules=[cost_validator(...)]list — the natural form most users reach for — it never receives per-request variables.get_argument_valuesthen raises aGraphQLErrorfor any argument supplied via a query variable, and every such query is rejected with a misleading "was not provided a runtime value" message.This PR makes that failure mode non-fatal: when
self.variables is None, the validator falls back to an empty args dict instead of reporting an error, so the query is allowed to run. Variable-driven multipliers are simply not counted in the cost. When the caller does pass an explicitvariablesdict, behaviour is unchanged — missing entries are still reported as validation errors (the caller has told the validator what is available).Fixes #1319.
Context
Root cause is in
ariadne/validation/query_cost.py, lines 92-98. Thetry/exceptaroundget_argument_valuesreported any exception viareport_error, which surfaces as a client-facing validation error and aborts the query. The function's signature (get_argument_values(field, child_node, variable_values=None)) raises on the first non-null variable it can't resolve, so for the static-rule form this triggers on every query that uses a non-null variable.The documented workaround — passing
validation_rulesas a per-request callable — keeps working unchanged; this fix only removes the spurious failure on the static form.Changes
ariadne/validation/query_cost.py— inCostValidator.compute_node_cost, branch onself.variables is None: silently fall back to emptyfield_args(cost is best-effort), otherwise preserve the existingreport_errorpath. Added a# See issue #1319.comment so a reviewer reading the diff cold understands the invariant.tests/test_query_cost_validation.py— four new regression tests covering: required scalar variable, required non-null input variable, best-effort cost computation with a missing variable, and explicitvariables={}still surfacing errors (guards against the fix loosening behaviour where the caller can actually catch it).Reproduce BEFORE/AFTER yourself (copy-paste)
What I ran locally
I also confirmed the three new "non-rejection" tests fail on
origin/mainby stashing onlyariadne/validation/query_cost.py(keeping the new tests):Edge cases
variables=None(default)query($v: Int!){ simple(value: $v) }default_complexityNonemutation($i: I!){ echo(input: $i) }None{ constant }{"v": 5}query($v: Int!){ simple(value: $v) }{}query($v: Int!){ simple(value: $v) }TypeError/ValueErrorfromcompute_costTypeError/ValueError(unchanged — differentexceptbranch)Risk / blast radius
The change is a one-line conditional in a single
exceptbranch inCostValidator.compute_node_cost. The default-arg semantics (variables: dict | None = None) are unchanged, so no callers need to update anything. Cost computation degrades gracefully — a query with variables that would multiply the cost by$value=1000will now under-count instead of refusing to run, which is the same trade-off the documented callable form makes between releases.The fix does not touch:
(TypeError, ValueError)handlers aroundcompute_cost(real misconfigurations are still reported).cost_mapvalidation inenter_operation_definition(schema-level errors are still reported).Nonevariablesdict, including{}(test 5 above).PR drafted with assistance from Claude Code (Anthropic). The change was reviewed manually against ariadne's source. The reproducer block above is the one I used during development; reviewers can paste it verbatim.
Summary by CodeRabbit
Bug Fixes
Tests