diff --git a/apisix/cli/config.lua b/apisix/cli/config.lua index 771c21bd339b..f50e8fe61db4 100644 --- a/apisix/cli/config.lua +++ b/apisix/cli/config.lua @@ -47,6 +47,7 @@ local _M = { }, delete_uri_tail_slash = false, normalize_uri_like_servlet = false, + max_post_args_readable_size = 64, router = { http = "radixtree_host_uri", ssl = "radixtree_sni" diff --git a/apisix/cli/schema.lua b/apisix/cli/schema.lua index 6673e565aca6..b57b30fed3ce 100644 --- a/apisix/cli/schema.lua +++ b/apisix/cli/schema.lua @@ -274,6 +274,13 @@ local config_schema = { }, uniqueItems = true }, + max_post_args_readable_size = { + type = "integer", + minimum = 0, + default = 64, + description = "cap (in MB) on the request body read for post_arg.* " + .. "route matching; 0 disables the limit", + }, } }, nginx_config = { diff --git a/apisix/core/ctx.lua b/apisix/core/ctx.lua index 1827254b6e07..64d1093312db 100644 --- a/apisix/core/ctx.lua +++ b/apisix/core/ctx.lua @@ -45,6 +45,8 @@ local pcall = pcall local _M = {version = 0.2} local GRAPHQL_DEFAULT_MAX_SIZE = 1048576 -- 1MiB +local DEFAULT_MAX_POST_ARGS_SIZE = 64 -- 64MiB +local MB = 1024 * 1024 local GRAPHQL_REQ_DATA_KEY = "query" local GRAPHQL_REQ_METHOD_HTTP_GET = "GET" local GRAPHQL_REQ_METHOD_HTTP_POST = "POST" @@ -129,6 +131,27 @@ local function parse_graphql(ctx) end +-- read the cap (in bytes) for parsing post_arg.* bodies; 0 disables the limit +local function get_max_post_args_readable_size() + local local_conf, err = config_local.local_conf() + if not local_conf then + log.error("failed to get local conf: ", err) + return DEFAULT_MAX_POST_ARGS_SIZE * MB + end + + local size = core_tab.try_read_attr(local_conf, "apisix", "max_post_args_readable_size") + if size == nil then + size = DEFAULT_MAX_POST_ARGS_SIZE + end + + if size == 0 then + return nil + end + + return size * MB +end + + local function get_parsed_graphql() local ctx = ngx.ctx.api_ctx if ctx._graphql then @@ -324,7 +347,8 @@ do elseif core_str.has_prefix(key, "post_arg.") then -- trim the "post_arg." prefix (10 characters) local arg_key = sub_str(key, 10) - local parsed_body, err = request.get_request_body_table(t._ctx) + local max_size = get_max_post_args_readable_size() + local parsed_body, err = request.get_request_body_table(t._ctx, nil, max_size) if not parsed_body then log.warn("failed to fetch post args value by key: ", arg_key, " error: ", err) return nil diff --git a/conf/config.yaml.example b/conf/config.yaml.example index 2360647e8f4a..345ddbfa711f 100644 --- a/conf/config.yaml.example +++ b/conf/config.yaml.example @@ -70,6 +70,8 @@ apisix: delete_uri_tail_slash: false # Delete the '/' at the end of the URI normalize_uri_like_servlet: false # If true, use the same path normalization rules as the Java # servlet specification. See https://github.com/jakartaee/servlet/blob/master/spec/src/main/asciidoc/servlet-spec-body.adoc#352-uri-path-canonicalization, which is used in Tomcat. + max_post_args_readable_size: 64 # Cap (in MB) on the request body read when matching `post_arg.*` + # route predicates for JSON and multipart requests. Set to 0 to disable the limit. router: http: radixtree_host_uri # radixtree_host_uri: match route by host and URI diff --git a/docs/en/latest/router-radixtree.md b/docs/en/latest/router-radixtree.md index c375360ef472..48ff8b167e39 100644 --- a/docs/en/latest/router-radixtree.md +++ b/docs/en/latest/router-radixtree.md @@ -413,3 +413,12 @@ curl -X POST http://127.0.0.1:9180/_post \ }' ``` + +:::note + +Matching `post_arg.*` against JSON or multipart bodies requires APISIX to read and parse the +request body during route matching. To avoid exhausting worker memory on large bodies, the read +is capped by `apisix.max_post_args_readable_size` in `config.yaml` (default `64` MB). Bodies larger +than this cap are not matched. Set it to `0` to disable the limit. + +::: diff --git a/t/core/ctx3.t b/t/core/ctx3.t index 058f91be6131..6764317d3c6e 100644 --- a/t/core/ctx3.t +++ b/t/core/ctx3.t @@ -227,3 +227,72 @@ hello world before rewrite model: gpt-4 after rewrite model: claude after rewrite temperature: 0.9 + + + +=== TEST 7: set route matching on post_arg.model +--- 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, + [=[{ + "methods": ["POST"], + "upstream": { + "nodes": { + "127.0.0.1:1980": 1 + }, + "type": "roundrobin" + }, + "uri": "/hello", + "vars": [ + ["post_arg.model", "==", "gpt-4"] + ] + }]=] + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 8: body over max_post_args_readable_size is not read, route not matched +--- yaml_config +apisix: + node_listen: 1984 + max_post_args_readable_size: 1 +--- request eval +"POST /hello +{\"model\":\"gpt-4\",\"pad\":\"" . ("a" x (2 * 1024 * 1024)) . "\"}" +--- more_headers +Content-Type: application/json +--- error_code: 404 +--- response_body +{"error_msg":"404 Route Not Found"} +--- error_log +is greater than the maximum size +--- no_error_log +[alert] + + + +=== TEST 9: body within max_post_args_readable_size still matches +--- yaml_config +apisix: + node_listen: 1984 + max_post_args_readable_size: 1 +--- request +POST /hello +{"model":"gpt-4"} +--- more_headers +Content-Type: application/json +--- response_body +hello world +--- error_code: 200