Skip to content

Use merged-field complexity limits with a separate occurrence budget - #4523

Open
andimarek wants to merge 13 commits into
masterfrom
codex/general-dos-protection
Open

andimarek wants to merge 13 commits into
masterfrom
codex/general-dos-protection

Conversation

@andimarek

@andimarek andimarek commented Nov 1, 2026 •

Copy link
Copy Markdown
Member

General QueryComplexityLimits now reuse Good Faith's merged-field calculation. The general limit is now named maxMergedFieldCount and changes from 100,000 expanded field occurrences to 10,000 merged fields, with a separate 100,000 field-occurrence limit so repeated selections remain bounded even when they merge.

Breaking changes and migration

The public API is renamed to make the counting change explicit. The old names are removed, with no compatibility aliases; callers must update their code and recompile.

Previous API Replacement
QueryComplexityLimits.Builder.maxFieldsCount(int) maxMergedFieldCount(int)
QueryComplexityLimits.getMaxFieldsCount() getMaxMergedFieldCount()
QueryComplexityLimits.DEFAULT_MAX_FIELDS_COUNT DEFAULT_MAX_MERGED_FIELD_COUNT
GoodFaithIntrospection.GOOD_FAITH_MAX_FIELDS_COUNT GOOD_FAITH_MAX_MERGED_FIELD_COUNT
ValidationErrorType.MaxQueryFieldsExceeded MaxQueryMergedFieldCountExceeded for merged-field failures; MaxQueryFieldOccurrencesExceeded for occurrence failures
  • Choose the intended metric when migrating: maxMergedFieldCount(N) limits merged validation fields. To retain the old expanded-field budget of maxFieldsCount(N), use maxFieldOccurrences(N); configure both when both budgets are needed. Duplicate selections can have fewer merged fields, while abstract-type grouping can produce more. The same number is not an equivalent policy for both metrics; examples are below.
  • Lower default merged-field limit: DEFAULT_MAX_MERGED_FIELD_COUNT is 10,000, replacing the old 100,000 expanded-field default. The separate default occurrence budget is 100,000. An operation with 10,001 distinct merged fields can now be rejected where it passed before. Good Faith's merged-field cap remains 500.
  • Custom builders still replace the defaults: unset limits retain Integer.MAX_VALUE. Setting only maxMergedFieldCount(N) does not enable an occurrence budget. Integer.MAX_VALUE disables occurrence counting independently.
  • Error handling changes: merged-field validation failures now use MaxQueryMergedFieldCountExceeded, and occurrence failures use MaxQueryFieldOccurrencesExceeded. For Good Faith operations, occurrence failures are now validation errors instead of BadFaithIntrospection; its merged-field/depth and coordinate failures still use BadFaithIntrospection. Merged-count messages explicitly say "merged fields". Earlier merged-field/depth checks can also change which failure is reported first. Update error-classification handling and exact-message assertions.
  • Consumers of @Internal types: GoodFaithIntrospectionCounts and FragmentComplexityInfo are removed; merged summaries use MergedFieldComplexityCounts. QueryComplexityLimitsExceeded now takes a long actual count and returns long from getActual(), changing its constructor/method binary signatures.

Limits and scope

Limits apply independently to every operation in the validated document, including unselected operations.

Default configuration Merged fields Field depth Expanded field occurrences
Ordinary query, mutation or subscription 10,000 100 100,000
Query selecting root __schema or __type, with Good Faith enabled 500 20 100,000

Good Faith covers the whole mixed operation, including ordinary fields. Aliases, fragments and skipped root introspection selections also activate it; __typename alone does not. Its existing restriction remains: at most one merged occurrence per operation of each root __schema, root __type, __Type.fields, __Type.inputFields, __Type.interfaces and __Type.possibleTypes. Different aliases or parent positions count separately.

  • Custom general limits apply to all operations; Good Faith uses the smaller configured merged-field/depth limit and its own 500/20 limits. It preserves the configured occurrence limit.
  • QueryComplexityLimits.NONE disables all three general limits; Good Faith remains independent. Disabling Good Faith leaves general limits active.
  • General limits remain active independently of the validation-rule predicate.

What the two field counts mean

Both object and leaf fields count. Merged counting combines selections by response name and common parent type at the same response position, including their children. Occurrence counting charges every field at every fragment use, without merging. Spread and inline-fragment nodes themselves do not count.

Selection Merged fields (maxMergedFieldCount) Field occurrences (maxFieldOccurrences; previously maxFieldsCount)
{ node { name name } } 2 3
{ node { ...F ...F } }, where F selects name 2 3
{ a: node { name } b: node { name } } 4 4

Depth is 2 in each example. All selections count regardless of @skip/@include; validation does not coerce variables or remove mutually exclusive type branches.

Merged counts can also exceed occurrence counts for abstract types. If Query.node returns interface I, implemented by A and B, and their child fields return I, A and B respectively:

{
  node {
    child { a: name b: name c: name }
    ... on A { child { name } }
    ... on B { child { name } }
  }
}

With name: String on all three types, this has 9 field occurrences and 11 merged validation fields: one node, plus five fields in each concrete-parent child group. These are the validator's groups, not ENO runtime-type expansion counts.

The committed 886-character fragment-doubling fixture has only two AST field nodes and two merged fields, but 33,554,433 field occurrences. The occurrence limit rejects it by default with MaxQueryFieldOccurrencesExceeded.

Implementation and protection boundaries

  • Share merged field/depth summaries between general limits and Good Faith. Reuse document-wide merge state, with per-operation count summaries keyed by validation-state identity.
  • Count leaves directly from merged selections; cache non-leaf summaries. Reuse singleton groups and traversal paths, and avoid repeated rule-array allocation.
  • Count occurrences during the existing validation walk, adding cached fragment totals at repeated spreads. Completed totals are shared across the document without skipping per-operation variable validation; incomplete cyclic/missing-fragment totals are not cached.
  • Check field depth before collecting children, retain checks through conflicting selections, use long counters to prevent overflow, and preserve conflict paths and ordering.

Merged counts bound response structure, not list lengths or resolver cost; the separate QueryComplexityCalculator API remains available. Occurrence counting follows initial merge validation, so it is not a preflight bound on all validation work. Parser limits, fragment-cycle detection and error limits remain necessary; this PR does not fix the existing stack overflow for very long acyclic fragment chains.

Verification and performance

  • Full local Java 25 suite passed: 6,058 tracked test cases, zero failures, eight skipped. Javadoc checks and the JMH build also pass after the API rename. Tests cover exact limits, aliases/fragments, all operation kinds, abstract types, directives, operation isolation, Good Faith, disabled rules, conflicts, cycles and overflow.
  • Java 25 Intel JMH/profiling across 20 workloads informed the merge-cache, traversal and direct-leaf optimizations. Both benchmark classes are included in this PR.
  • Latest occurrence-counter measurements on Java 25 / Apple M1 Pro: 10 workloads, 50 JVM forks, 45 seconds warmup and 30 seconds measurement per fork. Accepted-document counting-on/off comparisons showed increases of 0.4–1.8% on the larger workloads; the largest increase was 3.4% (+0.325 µs) on the 10-level doubling fixture. Comparisons against the preceding PR revision were +1.7–3.2%; mixed-interface repeats showed JVM variation. These measure the incremental occurrence counter, not the whole PR versus master. Parsing/schema setup were outside timing, and early rejection was measured separately.

@github-actions

github-actions Bot commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

Test Report

Test Results

Java Version Total Passed Failed Errors Skipped
Java 17 6277 (+100 🟢) 6221 (+100 🟢) 0 (±0) 0 (±0) 56 (±0)
Java 21 6277 (+100 🟢) 6221 (+100 🟢) 0 (±0) 0 (±0) 56 (±0)
Java 25 6277 (+100 🟢) 6221 (+100 🟢) 0 (±0) 0 (±0) 56 (±0)
jcstress 32 (±0) 32 (±0) 0 (±0) 0 (±0) 0 (±0)
Total 18863 (+300 🟢) 18695 (+300 🟢) 0 (±0) 0 (±0) 168 (±0)

Code Coverage (Java 25)

Metric Covered Missed Coverage vs Master
Lines 30338 3078 90.8% ±0.0%
Branches 8972 1504 85.6% +0.1% 🟢
Methods 8104 1186 87.2% ±0.0%

Changed Class Coverage (6 classes)

Class Line Branch Method
g.i.GoodFaithIntrospection +0.5% 🟢 ±0.0% ±0.0%
g.v.FragmentComplexityInfo removed removed removed
g.v.GoodFaithIntrospectionCounts removed removed removed
g.v.MergedFieldComplexityCounts +100.0% 🟢 +100.0% 🟢 +100.0% 🟢
g.v.OperationValidator +0.1% 🟢 +0.4% 🟢 ±0.0%
g.v.QueryComplexityLimits +7.1% 🟢 ±0.0% +12.5% 🟢

Full HTML report: build artifact jacoco-html-report

Updated: 2026-11-02 02:12:55 UTC

@andimarek andimarek changed the title Use merged-field counting for general query complexity limits Use merged-field complexity limits with a separate occurrence budget Nov 2, 2026
* Default maximum field count.
* Default maximum merged field count.
*/
public static final int DEFAULT_MAX_MERGED_FIELD_COUNT = 10_000;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is this too low - before we allowed 100_000 fields - so allowing for low "merging rates" - think this will catch out queries that were valid but had low "merge rates"

Is this too low as a default ??

This branch has not been deployed

No deployments
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.

2 participants