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
