Skip to content

feat(plugin): add graphql-limit-count plugin#13372

Open
AlinsRan wants to merge 11 commits into
masterfrom
feat/graphql-limit-count
Open

feat(plugin): add graphql-limit-count plugin#13372
AlinsRan wants to merge 11 commits into
masterfrom
feat/graphql-limit-count

Conversation

@AlinsRan
Copy link
Copy Markdown
Contributor

@AlinsRan AlinsRan commented May 14, 2026

What does this PR do?

Add a new graphql-limit-count plugin that limits GraphQL request rates based on the depth of the query Abstract Syntax Tree (AST) within a given time window.

Why is this change needed?

Standard rate limiting counts every request equally, regardless of its computational cost. This is insufficient for GraphQL endpoints where the cost of execution scales with query complexity. A single deeply nested query can be far more expensive than dozens of simple ones:

# Shallow query: depth 1, cheap to execute
query { users }

# Deep query: depth 5, potentially expensive (N+1 queries, large result sets)
query { users { orders { items { product { category { name } } } } } }

Without depth-aware limiting, a client could exhaust server resources with a single request while staying well within a request-count limit.

The graphql-limit-count plugin solves this by using the GraphQL query AST depth as the cost for each request, powered by the existing limit-count infrastructure. This lets operators configure a single depth budget per time window and have it consumed proportionally by query complexity.

How does it work?

  1. The plugin intercepts POST requests with application/json (containing a query field) or application/graphql content type.
  2. It parses the request body to extract the GraphQL query string.
  3. It calls graphql.parse() to build the AST and recursively counts the selections depth.
  4. It calls limit_count.rate_limit(conf, ctx, plugin_name, depth), passing the depth as the cost.
  5. If the accumulated cost within the time window exceeds count, the request is rejected with the configured rejected_code.

Example

Configure a route to allow a cumulative query depth of 10 per minute per client IP:

curl http://127.0.0.1:9180/apisix/admin/routes -X PUT \
  -H "X-API-KEY: ${admin_key}" \
  -d '{
    "id": "graphql-rate-limited",
    "uri": "/graphql",
    "plugins": {
      "graphql-limit-count": {
        "count": 10,
        "time_window": 60,
        "rejected_code": 429,
        "key": "remote_addr"
      }
    },
    "upstream": {
      "type": "roundrobin",
      "nodes": { "127.0.0.1:1980": 1 }
    }
  }'

A depth-4 query consumes 4 out of 10. After the budget is exhausted:

curl -i http://127.0.0.1:9080/graphql \
  -H 'Content-Type: application/json' \
  -d '{"query": "query { foo { bar { baz { id } } } }"}'

# HTTP/1.1 200 OK
# X-RateLimit-Limit: 10
# X-RateLimit-Remaining: 6

# (after quota exhaustion)
# HTTP/1.1 429 Too Many Requests

Changes

File Change
apisix/plugins/graphql-limit-count.lua New plugin (priority 1004)
t/plugin/graphql-limit-count.t 14 test cases covering normal flow, error handling, and Redis/Redis-cluster policies
apisix/cli/config.lua Register plugin; add plugin-graphql-limit-count and plugin-graphql-limit-count-reset-header shared dict defaults
apisix/cli/ngx_tpl.lua Conditional shared dict declarations for the plugin
conf/config.yaml.example Register plugin and shared dicts
docs/en/latest/plugins/graphql-limit-count.md English documentation with examples
docs/zh/latest/plugins/graphql-limit-count.md Chinese documentation with examples
docs/en/latest/config.json Add to Traffic sidebar
docs/zh/latest/config.json Add to 流量控制 sidebar
t/admin/plugins.t Register plugin in admin test list

Add a new plugin that limits GraphQL request rates based on query AST
depth using a fixed window algorithm. The plugin reuses the limit-count
infrastructure for counter management and supports local, Redis, and
Redis cluster policies.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@dosubot dosubot Bot added size:XL This PR changes 500-999 lines, ignoring generated files. doc Documentation things enhancement New feature or request plugin labels May 14, 2026
- Add Apache license header to t/plugin/graphql-limit-count.t
- Add plugin and shared dict entries to conf/config.yaml.example
- Rewrite EN/ZH docs to follow open-source format with Tabs and
  realistic curl examples showing expected response headers

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@dosubot dosubot Bot added size:XXL This PR changes 1000+ lines, ignoring generated files. and removed size:XL This PR changes 500-999 lines, ignoring generated files. labels May 14, 2026
@AlinsRan AlinsRan requested a review from Copilot May 14, 2026 05:52
- Add canonical link in <head> section
- Use correct attribute table format (Name/Type/Required/Default/Valid values/Description)
- Replace Tabs component with plain curl examples
- Follow the same structure as traffic-label and exit-transformer docs

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a new graphql-limit-count traffic control plugin that reuses limit-count infrastructure to charge requests by parsed GraphQL query depth.

Changes:

  • Adds the new plugin implementation and registers it in default plugin/admin configuration.
  • Adds NGINX shared dict configuration for the plugin.
  • Adds tests, documentation, and sidebar entries for English and Chinese docs.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 15 comments.

Show a summary per file
File Description
apisix/plugins/graphql-limit-count.lua Implements GraphQL parsing, depth calculation, and limit-count integration.
apisix/cli/config.lua Adds plugin and shared dict defaults.
apisix/cli/ngx_tpl.lua Adds conditional shared dict declarations.
conf/config.yaml.example Registers plugin and shared dicts in example config.
t/plugin/graphql-limit-count.t Adds plugin functional tests.
t/admin/plugins.t Adds plugin to admin plugin list expectations.
docs/en/latest/plugins/graphql-limit-count.md Adds English plugin documentation.
docs/zh/latest/plugins/graphql-limit-count.md Adds Chinese plugin documentation.
docs/en/latest/config.json Adds English sidebar entry.
docs/zh/latest/config.json Adds Chinese sidebar entry.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread apisix/plugins/graphql-limit-count.lua Outdated
Comment thread apisix/plugins/graphql-limit-count.lua Outdated
Comment thread apisix/plugins/graphql-limit-count.lua Outdated
Comment thread apisix/plugins/graphql-limit-count.lua Outdated
Comment thread t/plugin/graphql-limit-count.t
Comment thread apisix/plugins/graphql-limit-count.lua Outdated
Comment thread apisix/plugins/graphql-limit-count.lua Outdated
Comment thread apisix/plugins/graphql-limit-count.lua Outdated
Comment thread docs/zh/latest/plugins/graphql-limit-count.md Outdated
Comment thread apisix/cli/ngx_tpl.lua
@dosubot dosubot Bot added size:XL This PR changes 500-999 lines, ignoring generated files. and removed size:XXL This PR changes 1000+ lines, ignoring generated files. labels May 14, 2026
AlinsRan and others added 8 commits May 14, 2026 14:15
- fix max_query_depth: use proper max nesting depth traversal instead of
  counting total selection set nodes
- fix content-type matching: use has_prefix instead of exact equality
  to handle types with charset parameters (e.g. application/json; charset=utf-8)
- remove incorrect 'query' keyword check for application/graphql requests:
  shorthand queries and mutations are valid without 'query' keyword
- fix max_size: read graphql.max_size from local config instead of
  passing nil (no limit) to get_body
- fix typo: cant't -> can't in error message
- return specific error message from check_graphql_request instead of
  generic 'no query'
- fix ngx_tpl.lua: declare plugin-limit-count-redis-cluster-slot-lock
  dict when graphql-limit-count is enabled without limit-count
- fix tests: add no_shuffle(), redis flush in init_worker, rejection
  path test, application/graphql content-type success test, and update
  response_body assertions to match specific error messages
The shared dicts (plugin-graphql-limit-count,
plugin-graphql-limit-count-reset-header, and
plugin-limit-count-redis-cluster-slot-lock) are already generated by
ngx_tpl.lua when graphql-limit-count is enabled. Declaring them again
in http_config caused nginx to fail with 'already defined' error.
The test framework t/APISIX.pm maintains a hardcoded list of shared
dicts for the test nginx environment. New plugin dicts must be added
here, otherwise ngx.shared[dict] returns nil at request time.

Add plugin-graphql-limit-count and plugin-graphql-limit-count-reset-header
to the list, following the same pattern as plugin-ai-rate-limiting.
Separate test blocks cause nginx to restart between them, resetting the
shared dict counter. Merge the two rate limit checks (200 + 503) into a
single pipelined_requests block so both requests share the same nginx
instance and counter state.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

doc Documentation things enhancement New feature or request plugin size:XL This PR changes 500-999 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants