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

ashishtiwari pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix.git


The following commit(s) were added to refs/heads/master by this push:
     new 152077c22 feat: support ctx.var.post_arg for vars based route matching 
on request body (#12388)
152077c22 is described below

commit 152077c227ec2e07c2023c3ddef20bb83db93b35
Author: Ashish Tiwari <ashishjaitiwari15112...@gmail.com>
AuthorDate: Tue Jul 1 14:20:27 2025 +0530

    feat: support ctx.var.post_arg for vars based route matching on request 
body (#12388)
---
 apisix-master-0.rockspec           |   1 +
 apisix/admin/routes.lua            |  33 +++++
 apisix/core/ctx.lua                |  72 ++++++++++
 docs/en/latest/router-radixtree.md |  76 ++++++++++
 t/admin/routes_request_body.t      | 274 +++++++++++++++++++++++++++++++++++++
 5 files changed, 456 insertions(+)

diff --git a/apisix-master-0.rockspec b/apisix-master-0.rockspec
index c9432cc32..82ca9d8bb 100644
--- a/apisix-master-0.rockspec
+++ b/apisix-master-0.rockspec
@@ -81,6 +81,7 @@ dependencies = {
     "lua-resty-t1k = 1.1.5",
     "brotli-ffi = 0.3-1",
     "lua-ffi-zlib = 0.6-0",
+    "jsonpath = 1.0-1",
     "api7-lua-resty-aws == 2.0.2-1",
     "multipart = 0.5.9-1",
 }
diff --git a/apisix/admin/routes.lua b/apisix/admin/routes.lua
index 20187a4be..e13bb23e8 100644
--- a/apisix/admin/routes.lua
+++ b/apisix/admin/routes.lua
@@ -21,6 +21,33 @@ local resource = require("apisix.admin.resource")
 local schema_plugin = require("apisix.admin.plugins").check_schema
 local type = type
 local loadstring = loadstring
+local ipairs = ipairs
+local jp = require("jsonpath")
+
+local function validate_post_arg(node)
+    if type(node) ~= "table" then
+        return true
+    end
+
+    -- Handle post_arg conditions
+    if #node >= 3 and type(node[1]) == "string" and 
node[1]:find("^post_arg%.") then
+        local key = node[1]
+        local json_path = "$." .. key:sub(11)  -- Remove "post_arg." prefix
+        local _, err = jp.parse(json_path)
+        if err then
+            return false, err
+        end
+        return true
+    end
+
+    for _, child in ipairs(node) do
+        local ok, err = validate_post_arg(child)
+        if not ok then
+            return false, err
+        end
+    end
+    return true
+end
 
 
 local function check_conf(id, conf, need_id, schema)
@@ -111,6 +138,12 @@ local function check_conf(id, conf, need_id, schema)
         end
     end
 
+        ok, err = validate_post_arg(conf.vars)
+        if not ok  then
+            return nil, {error_msg = "failed to validate the 'vars' 
expression: " ..
+                                     err}
+        end
+
     if conf.filter_func then
         local func, err = loadstring("return " .. conf.filter_func)
         if not func then
diff --git a/apisix/core/ctx.lua b/apisix/core/ctx.lua
index e40ff8bb7..c6f66fbcc 100644
--- a/apisix/core/ctx.lua
+++ b/apisix/core/ctx.lua
@@ -29,7 +29,10 @@ local tablepool    = require("tablepool")
 local get_var      = require("resty.ngxvar").fetch
 local get_request  = require("resty.ngxvar").request
 local ck           = require "resty.cookie"
+local multipart    = require("multipart")
+local util         = require("apisix.cli.util")
 local gq_parse     = require("graphql").parse
+local jp           = require("jsonpath")
 local setmetatable = setmetatable
 local sub_str      = string.sub
 local ngx          = ngx
@@ -167,6 +170,47 @@ local function get_parsed_graphql()
 end
 
 
+local CONTENT_TYPE_JSON = "application/json"
+local CONTENT_TYPE_FORM_URLENCODED = "application/x-www-form-urlencoded"
+local CONTENT_TYPE_MULTIPART_FORM = "multipart/form-data"
+
+local function get_parsed_request_body(ctx)
+    local ct_header = request.header(ctx, "Content-Type") or ""
+
+    if core_str.find(ct_header, CONTENT_TYPE_JSON) then
+        local request_table, err = request.get_json_request_body_table()
+        if not request_table then
+            return nil, "failed to parse JSON body: " .. err
+        end
+        return request_table
+    end
+
+    if core_str.find(ct_header, CONTENT_TYPE_FORM_URLENCODED) then
+        local args, err = request.get_post_args()
+        if not args then
+            return nil, "failed to parse form data: " .. (err or "unknown 
error")
+        end
+        return args
+    end
+
+    if core_str.find(ct_header, CONTENT_TYPE_MULTIPART_FORM) then
+        local body = request.get_body()
+        local res = multipart(body, ct_header)
+        if not res then
+            return nil, "failed to parse multipart form data"
+        end
+        return res:get_all()
+    end
+
+    local err = "unsupported content-type in header: " .. ct_header ..
+                ", supported types are: " ..
+                CONTENT_TYPE_JSON .. ", " ..
+                CONTENT_TYPE_FORM_URLENCODED .. ", " ..
+                CONTENT_TYPE_MULTIPART_FORM
+    return nil, err
+end
+
+
 do
     local var_methods = {
         method = ngx.req.get_method,
@@ -292,6 +336,34 @@ do
                 -- trim the "graphql_" prefix
                 local arg_key = sub_str(key, 9)
                 val = get_parsed_graphql()[arg_key]
+            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 = get_parsed_request_body(t._ctx)
+                if not parsed_body then
+                    log.warn("failed to fetch post args value by key: ", 
arg_key, " error: ", err)
+                    return nil
+                end
+                if arg_key:find("[%[%*]") or arg_key:find("..", 1, true) then
+                    arg_key = "$." .. arg_key
+                    local results = jp.query(parsed_body, arg_key)
+                    if #results == 0 then
+                        val = nil
+                    else
+                        val = results
+                    end
+                else
+                    local parts = util.split(arg_key, "(.)")
+                    local current = parsed_body
+                    for _, part in ipairs(parts) do
+                        if type(current) ~= "table" then
+                            current = nil
+                            break
+                        end
+                        current = current[part]
+                    end
+                    val = current
+                end
 
             else
                 local getter = apisix_var_names[key]
diff --git a/docs/en/latest/router-radixtree.md 
b/docs/en/latest/router-radixtree.md
index b6e42e665..c375360ef 100644
--- a/docs/en/latest/router-radixtree.md
+++ b/docs/en/latest/router-radixtree.md
@@ -337,3 +337,79 @@ graphql:
 ```
 
 If you need to pass a GraphQL body which is larger than the limitation, you 
can increase the value in `conf/config.yaml`.
+
+### How to filter route by POST request JSON body?
+
+APISIX supports filtering route by POST form attributes with `Content-Type` = 
`application/json`.
+
+We can define the following route:
+
+```shell
+$ curl http://127.0.0.1:9180/apisix/admin/routes/1 -H "X-API-KEY: $admin_key" 
-X PUT -i -d '
+{
+    "methods": ["POST"],
+    "uri": "/_post",
+    "vars": [
+        ["post_arg.name", "==", "xyz"]
+    ],
+    "upstream": {
+        "type": "roundrobin",
+        "nodes": {
+            "127.0.0.1:1980": 1
+        }
+    }
+}'
+```
+
+It will match the following POST request
+
+```shell
+curl -X POST http://127.0.0.1:9180/_post \
+  -H "Content-Type: application/json" \
+  -d '{"name":"xyz"}'
+```
+
+We can also filter by complex queries like the example below:
+
+```shell
+$ curl http://127.0.0.1:9180/apisix/admin/routes/1 -H "X-API-KEY: $admin_key" 
-X PUT -i -d '
+{
+    "methods": ["POST"],
+    "uri": "/_post",
+    "vars": [
+         ["post_arg.messages[*].content[*].type","has","image_url"]
+    ],
+    "upstream": {
+        "type": "roundrobin",
+        "nodes": {
+            "127.0.0.1:1980": 1
+        }
+    }
+}'
+```
+
+It will match the following POST request
+
+```shell
+curl -X POST http://127.0.0.1:9180/_post \
+  -H "Content-Type: application/json" \
+  -d '{
+  "model": "deepseek",
+  "messages": [
+    {
+      "role": "system",
+      "content": [
+        {
+          "text": "You are a mathematician",
+          "type": "text"
+        },
+        {
+          "text": "You are a mathematician",
+          "type": "image_url"
+        }
+      ]
+    }
+  ]
+}'
+
+```
diff --git a/t/admin/routes_request_body.t b/t/admin/routes_request_body.t
new file mode 100644
index 000000000..4c4cb7119
--- /dev/null
+++ b/t/admin/routes_request_body.t
@@ -0,0 +1,274 @@
+#
+# 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.
+#
+
+use t::APISIX 'no_plan';
+
+log_level("info");
+repeat_each(1);
+no_long_string();
+no_root_location();
+
+add_block_preprocessor(sub {
+    my ($block) = @_;
+    if (!defined $block->request) {
+        $block->set_value("request", "GET /t");
+    }
+});
+
+run_tests();
+
+__DATA__
+
+=== TEST 1: set route in request body vars
+--- 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,
+                 [[{
+                    "uri": "/hello",
+                    "vars": [
+                        [
+                            ["post_arg.model","==", "deepseek"]
+                        ]
+                    ],
+                    "upstream": {
+                        "type": "roundrobin",
+                        "nodes": {
+                            "httpbin.org:80": 1
+                        }
+                    }
+                }]]
+            )
+
+            if code >= 300 then
+                ngx.status = code
+            end
+            local code, body = t('/apisix/admin/routes/2',
+                 ngx.HTTP_PUT,
+                 [[{
+                    "uri": "/hello",
+                    "vars": [
+                        [
+                            ["post_arg.model","==","openai"]
+                        ]
+                    ],
+                    "upstream": {
+                        "type": "roundrobin",
+                        "nodes": {
+                            "127.0.0.1:1980": 1
+                        }
+                    }
+                }]]
+            )
+            ngx.say(body)
+        }
+    }
+--- response_body
+passed
+
+
+
+=== TEST 2: send request with model == deepseek
+--- request
+POST /hello
+{ "model":"deepseek", "messages": [ { "role": "system", "content": "You are a 
mathematician" }] }
+--- more_headers
+Content-Type: application/json
+--- error_code: 404
+
+
+
+=== TEST 3: send request with model == openai and content-type == 
application/json
+--- request
+POST /hello
+{ "model":"openai", "messages": [ { "role": "system", "content": "You are a 
mathematician" }] }
+--- more_headers
+Content-Type: application/json
+--- error_code: 200
+
+
+
+=== TEST 4: send request with model == openai and content-type == 
application/x-www-form-urlencoded
+--- request
+POST /hello
+model=openai&messages[0][role]=system&messages[0][content]=You%20are%20a%20mathematician
+--- more_headers
+Content-Type: application/x-www-form-urlencoded
+--- error_code: 200
+
+
+
+=== TEST 5: multipart/form-data with model=openai
+--- request
+POST /hello
+--testboundary
+Content-Disposition: form-data; name="model"
+
+openai
+--testboundary--
+--- more_headers
+Content-Type: multipart/form-data; boundary=testboundary
+--- error_code: 200
+
+
+
+=== TEST 6: no match without content type
+--- request
+POST /hello
+--testboundary
+Content-Disposition: form-data; name="model"
+
+openai
+--testboundary--
+--- error_code: 404
+--- error_log
+unsupported content-type in header:
+
+
+
+=== TEST 7: use array in request body vars
+--- 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,
+                 [[{
+                    "uri": "/hello",
+                    "vars": [
+                        [
+                            
["post_arg.messages[*].content[*].type","has","image_url"]
+                        ]
+                    ],
+                    "upstream": {
+                        "type": "roundrobin",
+                        "nodes": {
+                            "127.0.0.1:1980": 1
+                        }
+                    }
+                }]]
+            )
+
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.say(body)
+        }
+    }
+--- response_body
+passed
+
+
+
+=== TEST 8: send request with type not image_url
+--- request
+POST /hello
+{ "model":"deepseek", "messages": [ { "role": "system", "content": 
[{"text":"You are a mathematician","type":"text"}] }] }
+--- more_headers
+Content-Type: application/json
+--- error_code: 404
+
+
+
+=== TEST 9: send request with type has image_url
+--- request
+POST /hello
+{ "model":"deepseek", "messages": [ { "role": "system", "content": 
[{"text":"You are a mathematician","type":"text"},{"text":"You are a 
mathematician","type":"image_url"}] }] }
+--- more_headers
+Content-Type: application/json
+--- error_code: 200
+
+
+
+=== TEST 10: use invalid jsonpath input
+--- 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,
+                 [[{
+                    "uri": "/hello",
+                    "vars": [
+                        [
+                            
["post_arg.messages[.content[*].type","has","image_url"]
+                        ]
+                    ],
+                    "upstream": {
+                        "type": "roundrobin",
+                        "nodes": {
+                            "127.0.0.1:1980": 1
+                        }
+                    }
+                }]]
+            )
+
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.say(body)
+        }
+    }
+--- response_body eval
+qr/.*failed to validate the 'vars' expression: invalid expression.*/
+--- error_code: 400
+
+
+
+=== TEST 11: use non array in request body vars
+--- 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,
+                 [[{
+                    "uri": "/hello",
+                    "vars": [
+                        [
+                            ["post_arg.model.name","==","deepseek"]
+                        ]
+                    ],
+                    "upstream": {
+                        "type": "roundrobin",
+                        "nodes": {
+                            "127.0.0.1:1980": 1
+                        }
+                    }
+                }]]
+            )
+
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.say(body)
+        }
+    }
+--- response_body
+passed
+
+
+
+=== TEST 12: send request
+--- request
+POST /hello
+{ "model":{"name": "deepseek"}, "messages": [ { "role": "system", "content": 
[{"text":"You are a mathematician","type":"text"},{"text":"You are a 
mathematician","type":"image_url"}] }] }
+--- more_headers
+Content-Type: application/json
+--- error_code: 200

Reply via email to