This is an automated email from the ASF dual-hosted git repository.

AlinsRan pushed a commit to branch feat/graphql-limit-count
in repository https://gitbox.apache.org/repos/asf/apisix.git

commit 3611ddb608d717182a6026580f5a7ea346645064
Author: AlinsRan <[email protected]>
AuthorDate: Thu May 14 13:38:09 2026 +0800

    feat(plugin): add graphql-limit-count plugin
    
    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 <[email protected]>
---
 apisix/cli/config.lua                         |   3 +
 apisix/cli/ngx_tpl.lua                        |   5 +
 apisix/plugins/graphql-limit-count.lua        | 147 ++++++++++++
 docs/en/latest/config.json                    |   1 +
 docs/en/latest/plugins/graphql-limit-count.md | 152 ++++++++++++
 docs/zh/latest/config.json                    |   1 +
 docs/zh/latest/plugins/graphql-limit-count.md | 152 ++++++++++++
 t/admin/plugins.t                             |   1 +
 t/plugin/graphql-limit-count.t                | 325 ++++++++++++++++++++++++++
 9 files changed, 787 insertions(+)

diff --git a/apisix/cli/config.lua b/apisix/cli/config.lua
index f6daf6e9d..3fb982b9a 100644
--- a/apisix/cli/config.lua
+++ b/apisix/cli/config.lua
@@ -170,6 +170,8 @@ local _M = {
         ["plugin-limit-req-redis-cluster-slot-lock"] = "1m",
         ["plugin-limit-count-redis-cluster-slot-lock"] = "1m",
         ["plugin-limit-conn-redis-cluster-slot-lock"] = "1m",
+        ["plugin-graphql-limit-count"] = "10m",
+        ["plugin-graphql-limit-count-reset-header"] = "10m",
         ["plugin-ai-rate-limiting"] = "10m",
         ["plugin-ai-rate-limiting-reset-header"] = "10m",
         tracing_buffer = "10m",
@@ -241,6 +243,7 @@ local _M = {
     "proxy-rewrite",
     "workflow",
     "api-breaker",
+    "graphql-limit-count",
     "limit-conn",
     "limit-count",
     "limit-req",
diff --git a/apisix/cli/ngx_tpl.lua b/apisix/cli/ngx_tpl.lua
index 6bd33368b..4cde4dcdd 100644
--- a/apisix/cli/ngx_tpl.lua
+++ b/apisix/cli/ngx_tpl.lua
@@ -327,6 +327,11 @@ http {
     lua_shared_dict plugin-limit-count-reset-header {* 
http.lua_shared_dict["plugin-limit-count"] *};
     {% end %}
 
+    {% if enabled_plugins["graphql-limit-count"] then %}
+    lua_shared_dict plugin-graphql-limit-count {* 
http.lua_shared_dict["plugin-graphql-limit-count"] *};
+    lua_shared_dict plugin-graphql-limit-count-reset-header {* 
http.lua_shared_dict["plugin-graphql-limit-count-reset-header"] *};
+    {% end %}
+
     {% if enabled_plugins["prometheus"] and not 
enabled_stream_plugins["prometheus"] then %}
     lua_shared_dict prometheus-metrics {* 
http.lua_shared_dict["prometheus-metrics"] *};
     {% end %}
diff --git a/apisix/plugins/graphql-limit-count.lua 
b/apisix/plugins/graphql-limit-count.lua
new file mode 100644
index 000000000..07e84782c
--- /dev/null
+++ b/apisix/plugins/graphql-limit-count.lua
@@ -0,0 +1,147 @@
+--
+-- Licensed to the Apache Software Foundation (ASF) under one or more
+-- contributor license agreements.  See the NOTICE file distributed with
+-- this work for additional information regarding copyright ownership.
+-- The ASF licenses this file to You under the Apache License, Version 2.0
+-- (the "License"); you may not use this file except in compliance with
+-- the License.  You may obtain a copy of the License at
+--
+--     http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+local limit_count     = require("apisix.plugins.limit-count.init")
+local core            = require("apisix.core")
+local gq_parse        = require("graphql").parse
+local limit_count_ver = require("resty.limit.count")._VERSION
+
+local type = type
+local pairs = pairs
+local pcall = pcall
+
+local plugin_name = "graphql-limit-count"
+local _M = {
+    version = 0.1,
+    priority = 1004,
+    name = plugin_name,
+    schema = limit_count.schema,
+}
+
+
+function _M.check_schema(conf)
+    return limit_count.check_schema(conf)
+end
+
+
+local GRAPHQL_REQ_QUERY     = "query"
+local GRAPHQL_REQ_MIME_JSON = "application/json"
+local GRAPHQL_REQ_MIME_GQL  = "application/graphql"
+
+
+local fetch_graphql_body = {
+    ["POST"] = function(ctx, max_size)
+        local body, err = core.request.get_body(max_size, ctx)
+        if not body then
+            return nil, "failed to read graphql data, " .. (err or "request 
body has zero size")
+        end
+
+        return body
+    end
+}
+
+
+local check_graphql_request = {
+    ["POST"] = function(ctx, body)
+        local content_type = core.request.header(ctx, "Content-Type")
+        if content_type == GRAPHQL_REQ_MIME_JSON then
+            local res, err = core.json.decode(body)
+            if not res then
+                return false, "invalid graphql request, " .. err
+            end
+
+            if not res[GRAPHQL_REQ_QUERY] then
+                return false, "invalid graphql request, json body[" ..
+                                GRAPHQL_REQ_QUERY .. "] is nil"
+            end
+
+            return true, res[GRAPHQL_REQ_QUERY]
+        end
+
+        if content_type == GRAPHQL_REQ_MIME_GQL then
+            if not core.string.find(body, GRAPHQL_REQ_QUERY) then
+                return false, "invalid graphql request, can't find '" ..
+                            GRAPHQL_REQ_QUERY .. "' in request body"
+            end
+            return true, body
+        end
+
+        return false, "invalid graphql request, error content-type: " .. 
(content_type or "")
+    end
+}
+
+
+-- Finds the depth of the graphql query from the given AST table.
+local function node_depth(t)
+    local depth = 0
+    if type(t) ~= "table" then
+        return depth
+    end
+
+    for k, v in pairs(t) do
+        if k == "selections" then
+            depth = depth + 1
+        end
+        depth = depth + node_depth(v)
+    end
+
+    return depth
+end
+
+
+function _M.access(conf, ctx)
+    if limit_count_ver < '1.0.0' then
+        core.log.error("need to build APISIX-Base to support GraphQL limit 
count")
+        return 501
+    end
+
+    local method = core.request.get_method()
+    if method ~= "POST" then
+        return 405
+    end
+
+    local body, err = fetch_graphql_body[method](ctx)
+    if not body then
+        core.log.error(err)
+        return 400, {message = "Invalid graphql request: cant't get graphql 
request body"}
+    end
+
+    local is_graphql_req, query_or_err = check_graphql_request[method](ctx, 
body)
+    if not is_graphql_req then
+        core.log.error(query_or_err)
+        return 400, {message = "Invalid graphql request: no query"}
+    end
+
+    local ok, res = pcall(gq_parse, query_or_err)
+    if not ok then
+        core.log.error("failed to parse graphql: ", res, ", body: ", body)
+        return 400, {message = "Invalid graphql request: failed to parse 
graphql query"}
+    end
+
+    local n = #res.definitions
+    if n == 0 then
+        core.log.error("failed to parse graphql: empty query, body: ", body)
+        return 400, {message = "Invalid graphql request: empty graphql query"}
+    end
+
+    local depth = node_depth(res)
+    core.log.info("graphql node depth: ", depth)
+
+    return limit_count.rate_limit(conf, ctx, plugin_name, depth)
+end
+
+
+return _M
diff --git a/docs/en/latest/config.json b/docs/en/latest/config.json
index 6de6e1d92..118969e91 100644
--- a/docs/en/latest/config.json
+++ b/docs/en/latest/config.json
@@ -161,6 +161,7 @@
             "plugins/limit-req",
             "plugins/limit-conn",
             "plugins/limit-count",
+            "plugins/graphql-limit-count",
             "plugins/proxy-cache",
             "plugins/request-validation",
             "plugins/oas-validator",
diff --git a/docs/en/latest/plugins/graphql-limit-count.md 
b/docs/en/latest/plugins/graphql-limit-count.md
new file mode 100644
index 000000000..0395c652c
--- /dev/null
+++ b/docs/en/latest/plugins/graphql-limit-count.md
@@ -0,0 +1,152 @@
+---
+title: graphql-limit-count
+keywords:
+  - Apache APISIX
+  - API Gateway
+  - Plugin
+  - graphql-limit-count
+  - Rate Limiting
+  - GraphQL
+description: The graphql-limit-count Plugin limits the rate of GraphQL 
requests by the depth of the query AST within a given time window, using the 
same counting mechanism as the limit-count Plugin.
+---
+
+<!--
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+-->
+
+## Description
+
+The `graphql-limit-count` Plugin limits the rate of GraphQL requests using a 
fixed window algorithm. Unlike `limit-count`, which counts each request as a 
cost of 1, this plugin uses the **depth of the GraphQL query AST** as the cost. 
This lets you enforce stricter limits on deeply nested queries that are more 
expensive to process.
+
+Only `POST` requests are supported. The request body must use either 
`application/json` (with a `query` field) or `application/graphql` content type.
+
+You may see the following rate limiting headers in the response:
+
+- `X-RateLimit-Limit`: the total quota
+- `X-RateLimit-Remaining`: the remaining quota
+- `X-RateLimit-Reset`: number of seconds left for the counter to reset
+
+## Attributes
+
+This plugin shares the same schema as the [limit-count](./limit-count.md) 
plugin. All attributes from `limit-count` apply here.
+
+| Name                    | Type              | Required | Default       | 
Valid values                           | Description                            
                                                                  |
+| ----------------------- | ----------------- | -------- | ------------- | 
-------------------------------------- | 
--------------------------------------------------------------------------------------------------------
 |
+| count                   | integer or string | False    |               | > 0 
                                   | The maximum allowed accumulated GraphQL 
query depth within the time window. Required if `rules` is not configured. |
+| time_window             | integer or string | False    |               | > 0 
                                   | The time interval in seconds. Required if 
`rules` is not configured.                                    |
+| key_type                | string            | False    | var           | 
["var","var_combination","constant"]   | The type of key.                       
                                                                  |
+| key                     | string            | False    | remote_addr   |     
                                   | The key to count requests by.              
                                                              |
+| rejected_code           | integer           | False    | 503           | 
[200,...,599]                          | The HTTP status code returned when a 
request is rejected.                                                |
+| rejected_msg            | string            | False    |               | 
non-empty                              | The response body returned when a 
request is rejected.                                                   |
+| policy                  | string            | False    | local         | 
["local","redis","redis-cluster"]      | The policy for the rate limiting 
counter.                                                                |
+| allow_degradation       | boolean           | False    | false         |     
                                   | If true, APISIX continues handling 
requests when the plugin or its dependencies are unavailable.         |
+| show_limit_quota_header | boolean           | False    | true          |     
                                   | If true, include rate limiting headers in 
the response.                                                  |
+| group                   | string            | False    |               | 
non-empty                              | Group ID for sharing the rate limiting 
counter across routes.                                            |
+| redis_host              | string            | False    |               |     
                                   | Address of the Redis node. Required when 
`policy` is `redis`.                                            |
+| redis_port              | integer           | False    | 6379          | 
[1,...]                                | Port of the Redis node when `policy` 
is `redis`.                                                         |
+| redis_username          | string            | False    |               |     
                                   | Username for Redis ACL authentication when 
`policy` is `redis`.                                          |
+| redis_password          | string            | False    |               |     
                                   | Password of the Redis node when `policy` 
is `redis` or `redis-cluster`.                                  |
+| redis_ssl               | boolean           | False    | false         |     
                                   | If true, use SSL to connect to Redis when 
`policy` is `redis`.                                           |
+| redis_ssl_verify        | boolean           | False    | false         |     
                                   | If true, verify the server SSL certificate 
when `policy` is `redis`.                                     |
+| redis_database          | integer           | False    | 0             | >= 
0                                   | The database number in Redis when 
`policy` is `redis`.                                                   |
+| redis_timeout           | integer           | False    | 1000          | 
[1,...]                                | The Redis timeout in milliseconds when 
`policy` is `redis` or `redis-cluster`.                           |
+| redis_cluster_nodes     | array[string]     | False    |               |     
                                   | List of Redis cluster nodes. Required when 
`policy` is `redis-cluster`.                                  |
+| redis_cluster_name      | string            | False    |               |     
                                   | Name of the Redis cluster. Required when 
`policy` is `redis-cluster`.                                    |
+| redis_cluster_ssl       | boolean           | False    | false         |     
                                   | If true, use SSL to connect to the Redis 
cluster when `policy` is `redis-cluster`.                       |
+| redis_cluster_ssl_verify| boolean           | False    | false         |     
                                   | If true, verify the server SSL certificate 
when `policy` is `redis-cluster`.                             |
+
+## Examples
+
+The examples below demonstrate how you can configure `graphql-limit-count` for 
different scenarios.
+
+:::note
+
+You can fetch the `admin_key` from `config.yaml` and save to an environment 
variable with the following command:
+
+```bash
+admin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 
's/"//g')
+```
+
+:::
+
+### Limit by GraphQL query depth (local policy)
+
+The following example demonstrates how to rate limit GraphQL requests based on 
query depth using an in-memory counter.
+
+Create a route with `graphql-limit-count`:
+
+```shell
+curl http://127.0.0.1:9180/apisix/admin/routes/1 \
+  -H "X-API-KEY: $admin_key" -X PUT -d '
+{
+  "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
+    }
+  }
+}'
+```
+
+Send a GraphQL `POST` request:
+
+```shell
+curl -i http://127.0.0.1:9080/graphql \
+  -H "Content-Type: application/json" \
+  -d '{"query": "query { foo { bar { baz } } }"}'
+```
+
+The response should include `X-RateLimit-Remaining` showing the remaining 
quota. The cost is the depth of the query AST (3 in this case), so this request 
consumes 3 out of 10.
+
+### Limit by GraphQL query depth (Redis policy)
+
+The following example demonstrates using a Redis-backed counter to share state 
across multiple APISIX nodes.
+
+```shell
+curl http://127.0.0.1:9180/apisix/admin/routes/1 \
+  -H "X-API-KEY: $admin_key" -X PUT -d '
+{
+  "uri": "/graphql",
+  "plugins": {
+    "graphql-limit-count": {
+      "count": 100,
+      "time_window": 60,
+      "rejected_code": 429,
+      "key": "remote_addr",
+      "policy": "redis",
+      "redis_host": "127.0.0.1",
+      "redis_port": 6379
+    }
+  },
+  "upstream": {
+    "type": "roundrobin",
+    "nodes": {
+      "127.0.0.1:1980": 1
+    }
+  }
+}'
+```
diff --git a/docs/zh/latest/config.json b/docs/zh/latest/config.json
index 074c571eb..d28611fb8 100644
--- a/docs/zh/latest/config.json
+++ b/docs/zh/latest/config.json
@@ -150,6 +150,7 @@
             "plugins/limit-req",
             "plugins/limit-conn",
             "plugins/limit-count",
+            "plugins/graphql-limit-count",
             "plugins/proxy-cache",
             "plugins/request-validation",
             "plugins/oas-validator",
diff --git a/docs/zh/latest/plugins/graphql-limit-count.md 
b/docs/zh/latest/plugins/graphql-limit-count.md
new file mode 100644
index 000000000..6a6b988f0
--- /dev/null
+++ b/docs/zh/latest/plugins/graphql-limit-count.md
@@ -0,0 +1,152 @@
+---
+title: graphql-limit-count
+keywords:
+  - Apache APISIX
+  - API Gateway
+  - Plugin
+  - graphql-limit-count
+  - 限流
+  - GraphQL
+description: graphql-limit-count 插件通过在指定时间窗口内累计 GraphQL 查询 AST 深度来限制请求速率,采用与 
limit-count 相同的计数机制。
+---
+
+<!--
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+-->
+
+## 描述
+
+`graphql-limit-count` 插件使用固定窗口算法对 GraphQL 请求进行速率限制。与每次请求消耗固定计数 1 的 
`limit-count` 不同,本插件以 **GraphQL 查询 AST 的深度**作为消耗代价,从而对嵌套层级更深、处理代价更高的查询施加更严格的限制。
+
+仅支持 `POST` 方法。请求体必须使用 `application/json`(含 `query` 字段)或 `application/graphql` 
内容类型。
+
+响应中可能包含以下限流相关的响应头:
+
+- `X-RateLimit-Limit`:总配额
+- `X-RateLimit-Remaining`:剩余配额
+- `X-RateLimit-Reset`:计数器重置的剩余秒数
+
+## 属性
+
+本插件与 [limit-count](./limit-count.md) 插件共享相同的 Schema,`limit-count` 的所有属性均适用。
+
+| 名称                    | 类型              | 必填 | 默认值        | 有效值              
                   | 描述                                                         
                                      |
+| ----------------------- | ----------------- | ---- | ------------- | 
-------------------------------------- | 
--------------------------------------------------------------------------------------------------
 |
+| count                   | integer or string | 否   |               | > 0      
                              | 时间窗口内允许的最大 GraphQL 查询深度累计值。当未配置 `rules` 时必填。    
                         |
+| time_window             | integer or string | 否   |               | > 0      
                              | 限流计数对应的时间窗口(秒)。当未配置 `rules` 时必填。                
                            |
+| key_type                | string            | 否   | var           | 
["var","var_combination","constant"]   | key 的类型。                               
                                                        |
+| key                     | string            | 否   | remote_addr   |          
                              | 用于计数的 key。                                      
                                             |
+| rejected_code           | integer           | 否   | 503           | 
[200,...,599]                          | 请求被拒绝时返回的 HTTP 状态码。                    
                                               |
+| rejected_msg            | string            | 否   |               | 非空       
                            | 请求被拒绝时返回的响应体。                                     
                                    |
+| policy                  | string            | 否   | local         | 
["local","redis","redis-cluster"]      | 限流计数器的存储策略。                            
                                                 |
+| allow_degradation       | boolean           | 否   | false         |          
                              | 为 true 时,插件或依赖不可用时 APISIX 仍继续处理请求。              
                              |
+| show_limit_quota_header | boolean           | 否   | true          |          
                              | 为 true 时,在响应中包含限流相关的响应头。                        
                                 |
+| group                   | string            | 否   |               | 非空       
                            | 用于在多个路由之间共享限流计数器的 Group ID。                       
                              |
+| redis_host              | string            | 否   |               |          
                              | Redis 节点地址。`policy` 为 `redis` 时必填。              
                                        |
+| redis_port              | integer           | 否   | 6379          | [1,...]  
                              | `policy` 为 `redis` 时 Redis 节点的端口。               
                                         |
+| redis_username          | string            | 否   |               |          
                              | 使用 Redis ACL 认证时的用户名。`policy` 为 `redis` 时使用。    
                                  |
+| redis_password          | string            | 否   |               |          
                              | `policy` 为 `redis` 或 `redis-cluster` 时 Redis 
节点的密码。                                     |
+| redis_ssl               | boolean           | 否   | false         |          
                              | 为 true 时,`policy` 为 `redis` 时使用 SSL 连接 Redis。   
                                       |
+| redis_ssl_verify        | boolean           | 否   | false         |          
                              | 为 true 时,验证 `policy` 为 `redis` 时服务端的 SSL 证书。    
                                   |
+| redis_database          | integer           | 否   | 0             | >= 0     
                              | `policy` 为 `redis` 时使用的 Redis 数据库编号。            
                                      |
+| redis_timeout           | integer           | 否   | 1000          | [1,...]  
                              | `policy` 为 `redis` 或 `redis-cluster` 时的 Redis 
超时时间(毫秒)。                             |
+| redis_cluster_nodes     | array[string]     | 否   |               |          
                              | Redis 集群节点列表。`policy` 为 `redis-cluster` 时必填。    
                                     |
+| redis_cluster_name      | string            | 否   |               |          
                              | Redis 集群名称。`policy` 为 `redis-cluster` 时必填。      
                                       |
+| redis_cluster_ssl       | boolean           | 否   | false         |          
                              | 为 true 时,`policy` 为 `redis-cluster` 时使用 SSL 连接 
Redis 集群。                             |
+| redis_cluster_ssl_verify| boolean           | 否   | false         |          
                              | 为 true 时,验证 `policy` 为 `redis-cluster` 时服务端的 
SSL 证书。                               |
+
+## 示例
+
+以下示例演示了如何在不同场景下配置 `graphql-limit-count` 插件。
+
+:::note
+
+您可以用以下命令从 `config.yaml` 中获取 `admin_key` 并存入环境变量:
+
+```bash
+admin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 
's/"//g')
+```
+
+:::
+
+### 基于 GraphQL 查询深度限流(本地策略)
+
+以下示例演示如何使用内存计数器对 GraphQL 请求按查询深度进行速率限制。
+
+创建带有 `graphql-limit-count` 的路由:
+
+```shell
+curl http://127.0.0.1:9180/apisix/admin/routes/1 \
+  -H "X-API-KEY: $admin_key" -X PUT -d '
+{
+  "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
+    }
+  }
+}'
+```
+
+发送 GraphQL `POST` 请求:
+
+```shell
+curl -i http://127.0.0.1:9080/graphql \
+  -H "Content-Type: application/json" \
+  -d '{"query": "query { foo { bar { baz } } }"}'
+```
+
+响应中将包含 `X-RateLimit-Remaining`,显示剩余配额。此查询的 AST 深度为 3,因此本次请求消耗 10 中的 3 个配额。
+
+### 基于 GraphQL 查询深度限流(Redis 策略)
+
+以下示例演示如何使用 Redis 后端计数器,在多个 APISIX 节点之间共享状态。
+
+```shell
+curl http://127.0.0.1:9180/apisix/admin/routes/1 \
+  -H "X-API-KEY: $admin_key" -X PUT -d '
+{
+  "uri": "/graphql",
+  "plugins": {
+    "graphql-limit-count": {
+      "count": 100,
+      "time_window": 60,
+      "rejected_code": 429,
+      "key": "remote_addr",
+      "policy": "redis",
+      "redis_host": "127.0.0.1",
+      "redis_port": 6379
+    }
+  },
+  "upstream": {
+    "type": "roundrobin",
+    "nodes": {
+      "127.0.0.1:1980": 1
+    }
+  }
+}'
+```
diff --git a/t/admin/plugins.t b/t/admin/plugins.t
index b9f3846ca..84674717b 100644
--- a/t/admin/plugins.t
+++ b/t/admin/plugins.t
@@ -108,6 +108,7 @@ proxy-mirror
 proxy-rewrite
 workflow
 api-breaker
+graphql-limit-count
 limit-conn
 limit-count
 limit-req
diff --git a/t/plugin/graphql-limit-count.t b/t/plugin/graphql-limit-count.t
new file mode 100644
index 000000000..8a61dabb7
--- /dev/null
+++ b/t/plugin/graphql-limit-count.t
@@ -0,0 +1,325 @@
+use t::APISIX 'no_plan';
+
+repeat_each(1);
+no_long_string();
+no_root_location();
+
+add_block_preprocessor(sub {
+    my ($block) = @_;
+
+    my $http_config = $block->http_config // <<_EOC_;
+    lua_shared_dict plugin-graphql-limit-count 10m;
+    lua_shared_dict plugin-graphql-limit-count-reset-header 10m;
+_EOC_
+
+    $block->set_value("http_config", $http_config);
+
+    my $extra_yaml_config = $block->extra_yaml_config // <<_EOC_;
+plugins:
+    - graphql-limit-count
+_EOC_
+
+    $block->set_value("extra_yaml_config", $extra_yaml_config);
+
+    if (!$block->request) {
+        $block->set_value("request", "GET /t");
+    }
+
+    if ((!defined $block->error_log) && (!defined $block->no_error_log)) {
+        $block->set_value("no_error_log", "[error]");
+    }
+});
+
+run_tests;
+
+__DATA__
+
+=== TEST 1: set route: normal
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            local code, body = t('/apisix/admin/routes/1',
+                 ngx.HTTP_PUT,
+                 [[{
+                    "plugins": {
+                        "graphql-limit-count": {
+                            "count": 4,
+                            "time_window": 60,
+                            "rejected_code": 503,
+                            "key": "remote_addr"
+                        }
+                    },
+                    "upstream": {
+                        "nodes": {
+                            "127.0.0.1:1980": 1
+                        },
+                        "type": "roundrobin"
+                    },
+                    "uri": "/hello"
+                }]]
+                )
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.say(body)
+        }
+    }
+--- request
+GET /t
+--- response_body
+passed
+
+
+
+=== TEST 2: hit - query with depth equal to 4
+--- request
+POST /hello
+{
+  "query": "query awesomeGraphqlQuery { foo { bar, baz { boo, bee, baa { 
bar_id, lol } } } }"
+}
+--- more_headers
+Content-Type: application/json
+--- error_code eval
+200
+--- response_headers
+X-RateLimit-Remaining: 0
+
+
+
+=== TEST 3: invalid graphql request: wrong method
+--- request
+HEAD /hello
+--- error_code: 405
+
+
+
+=== TEST 4: invalid graphql request: post method without body
+--- request
+POST /hello
+--- error_code: 400
+--- error_log
+failed to read graphql data, request body has zero size
+--- response_body eval
+qr/Invalid graphql request: cant't get graphql request body/
+
+
+
+=== TEST 5: invalid graphql request: wrong content-type
+--- request
+POST /hello
+{
+    "query": "query{persons{id}}"
+}
+--- error_code: 400
+--- error_log
+invalid graphql request, error content-type
+--- response_body eval
+qr/Invalid graphql request: no query/
+
+
+
+=== TEST 6: invalid graphql request: wrong json
+--- request
+POST /hello
+{
+    "query": "query{persons{id}}",
+}
+--- more_headers
+Content-Type: application/json
+--- error_code: 400
+--- error_log
+invalid graphql request, Expected object key string but found T_OBJ_END at 
character 38
+--- response_body eval
+qr/Invalid graphql request: no query/
+
+
+
+=== TEST 7: invalid graphql request: no query
+--- request
+POST /hello
+{
+    "test": "query{persons{id}}"
+}
+--- more_headers
+Content-Type: application/json
+--- error_code: 400
+--- error_log
+invalid graphql request, json body[query] is nil
+--- response_body eval
+qr/Invalid graphql request: no query/
+
+
+
+=== TEST 8: invalid graphql request: graphql data no query
+--- request
+POST /hello
+test {
+  persons(filter: { name: "Niek" }) {
+    name
+    blog
+    githubAccount
+  }
+}
+--- more_headers
+Content-Type: application/graphql
+--- error_code: 400
+--- error_log
+invalid graphql request, can't find 'query' in request body
+--- response_body eval
+qr/Invalid graphql request: no query/
+
+
+
+=== TEST 9: invalid graphql request: failed to parse graphql
+--- request
+POST /hello
+{
+    "query": "query{persons(filter){id}}"
+}
+--- more_headers
+Content-Type: application/json
+--- error_code: 400
+--- error_log eval
+qr/failed to parse graphql: Syntax error near line 1/
+--- response_body eval
+qr/Invalid graphql request: failed to parse graphql query/
+
+
+
+=== TEST 10: invalid graphql request: empty query
+--- request
+POST /hello
+{
+    "query": ""
+}
+--- more_headers
+Content-Type: application/json
+--- error_code: 400
+--- error_log eval
+qr/failed to parse graphql: empty query/
+--- response_body eval
+qr/Invalid graphql request: empty graphql query/
+
+
+
+=== TEST 11: set route: graphql limit-count with redis policy
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            local code, body = t('/apisix/admin/routes/1',
+                 ngx.HTTP_PUT,
+                 [[{
+                    "plugins": {
+                        "graphql-limit-count": {
+                            "allow_degradation": false,
+                            "rejected_code": 503,
+                            "redis_timeout": 1000,
+                            "key_type": "var",
+                            "time_window": 60,
+                            "show_limit_quota_header": true,
+                            "count": 5,
+                            "redis_host": "127.0.0.1",
+                            "redis_port": 6379,
+                            "redis_database": 0,
+                            "policy": "redis",
+                            "key": "remote_addr"
+                        }
+                    },
+                    "upstream": {
+                        "nodes": {
+                            "127.0.0.1:1980": 1
+                        },
+                        "type": "roundrobin"
+                    },
+                    "uri": "/hello"
+                }]]
+                )
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.say(body)
+        }
+    }
+--- request
+GET /t
+--- response_body
+passed
+
+
+
+=== TEST 12: hit redis policy - query with depth equal to 4
+--- request
+POST /hello
+{
+  "query": "query awesomeGraphqlQuery { foo { bar, baz { boo, bee, baa { 
bar_id, lol } } } }"
+}
+--- more_headers
+Content-Type: application/json
+--- error_code eval
+200
+--- response_headers
+X-RateLimit-Remaining: 1
+
+
+
+=== TEST 13: set route: graphql limit-count with redis-cluster policy
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin").test
+            local code, body = t('/apisix/admin/routes/1',
+                 ngx.HTTP_PUT,
+                 [[{
+                    "plugins": {
+                        "graphql-limit-count": {
+                            "redis_cluster_nodes": ["127.0.0.1:5000", 
"127.0.0.1:5001"],
+                            "redis_cluster_name": "redis-cluster-1",
+                            "redis_cluster_ssl": false,
+                            "redis_timeout": 1000,
+                            "key_type": "var",
+                            "time_window": 60,
+                            "show_limit_quota_header": true,
+                            "allow_degradation": false,
+                            "key": "remote_addr",
+                            "rejected_code": 503,
+                            "count": 5,
+                            "policy": "redis-cluster",
+                            "redis_cluster_ssl_verify": false
+                        }
+                    },
+                    "upstream": {
+                        "nodes": {
+                            "127.0.0.1:1980": 1
+                        },
+                        "type": "roundrobin"
+                    },
+                    "uri": "/hello"
+                }]]
+                )
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.say(body)
+        }
+    }
+--- request
+GET /t
+--- response_body
+passed
+
+
+
+=== TEST 14: hit redis-cluster policy - query with depth equal to 4
+--- request
+POST /hello
+{
+  "query": "query awesomeGraphqlQuery { foo { bar, baz { boo, bee, baa { 
bar_id, lol } } } }"
+}
+--- more_headers
+Content-Type: application/json
+--- error_code eval
+200
+--- response_headers
+X-RateLimit-Remaining: 1


Reply via email to