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

young 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 20481ab27 feat(standalone): support JSON format (#12333)
20481ab27 is described below

commit 20481ab27113e055f6ba1a2510a8e433efe64e54
Author: YYYoung <[email protected]>
AuthorDate: Sat Jun 21 18:56:46 2025 +0800

    feat(standalone): support JSON format (#12333)
    
    Co-authored-by: Traky Deng <[email protected]>
---
 apisix/cli/file.lua                    |   2 +
 apisix/cli/schema.lua                  |   2 +-
 apisix/core.lua                        |  11 +-
 apisix/core/config_yaml.lua            | 113 +++++---
 docs/en/latest/deployment-modes.md     | 464 ++++++++++++++++++++++++++++++++-
 t/APISIX.pm                            |  24 ++
 t/config-center-json/consumer-group.t  | 212 +++++++++++++++
 t/config-center-json/consumer.t        |  93 +++++++
 t/config-center-json/global-rule.t     | 160 ++++++++++++
 t/config-center-json/plugin-configs.t  | 175 +++++++++++++
 t/config-center-json/plugin-metadata.t | 100 +++++++
 t/config-center-json/plugin.t          | 291 +++++++++++++++++++++
 t/config-center-json/route-service.t   | 379 +++++++++++++++++++++++++++
 t/config-center-json/route-upstream.t  | 244 +++++++++++++++++
 t/config-center-json/route.t           | 364 ++++++++++++++++++++++++++
 t/config-center-json/secret.t          | 458 ++++++++++++++++++++++++++++++++
 t/config-center-json/ssl.t             | 191 ++++++++++++++
 t/config-center-json/stream-route.t    | 148 +++++++++++
 18 files changed, 3384 insertions(+), 47 deletions(-)

diff --git a/apisix/cli/file.lua b/apisix/cli/file.lua
index e4d2a3161..36873631a 100644
--- a/apisix/cli/file.lua
+++ b/apisix/cli/file.lua
@@ -287,6 +287,8 @@ function _M.read_yaml_conf(apisix_home)
             default_conf.etcd = default_conf.deployment.etcd
             if default_conf.deployment.role_data_plane.config_provider == 
"yaml" then
                 default_conf.deployment.config_provider = "yaml"
+            elseif default_conf.deployment.role_data_plane.config_provider == 
"json" then
+                default_conf.deployment.config_provider = "json"
             elseif default_conf.deployment.role_data_plane.config_provider == 
"xds" then
                 default_conf.deployment.config_provider = "xds"
             end
diff --git a/apisix/cli/schema.lua b/apisix/cli/schema.lua
index 6e60a9166..36d758ca8 100644
--- a/apisix/cli/schema.lua
+++ b/apisix/cli/schema.lua
@@ -405,7 +405,7 @@ local deployment_schema = {
             role_data_plane = {
                 properties = {
                     config_provider = {
-                        enum = {"etcd", "yaml", "xds"}
+                        enum = {"etcd", "yaml", "json", "xds"}
                     },
                 },
                 required = {"config_provider"}
diff --git a/apisix/core.lua b/apisix/core.lua
index 8e23b8ed0..14c5186a4 100644
--- a/apisix/core.lua
+++ b/apisix/core.lua
@@ -24,7 +24,16 @@ end
 local config_provider = local_conf.deployment and 
local_conf.deployment.config_provider
                       or "etcd"
 log.info("use config_provider: ", config_provider)
-local config = require("apisix.core.config_" .. config_provider)
+
+local config
+-- Currently, we handle JSON parsing in config_yaml, so special processing is 
needed here.
+if config_provider == "json" then
+    config = require("apisix.core.config_yaml")
+    config.file_type = "json"
+else
+    config = require("apisix.core.config_" .. config_provider)
+end
+
 config.type = config_provider
 
 
diff --git a/apisix/core/config_yaml.lua b/apisix/core/config_yaml.lua
index f7f54e18d..747b08743 100644
--- a/apisix/core/config_yaml.lua
+++ b/apisix/core/config_yaml.lua
@@ -46,7 +46,6 @@ local ngx          = ngx
 local re_find      = ngx.re.find
 local process      = require("ngx.process")
 local worker_id    = ngx.worker.id
-local apisix_yaml_path = profile:yaml_path("apisix")
 local created_obj  = {}
 local shared_dict
 local status_report_shared_dict_name = "status-report"
@@ -56,6 +55,9 @@ local _M = {
     local_conf = config_local.local_conf,
     clear_local_cache = config_local.clear_cache,
 
+    -- yaml or json
+    file_type = "yaml",
+
     ERR_NO_SHARED_DICT = "failed prepare standalone config shared dict, this 
will degrade "..
                     "to event broadcasting, and if a worker crashes, the 
configuration "..
                     "cannot be restored from other workers and shared dict"
@@ -69,10 +71,68 @@ local mt = {
     end
 }
 
-
 local apisix_yaml
 local apisix_yaml_mtime
 
+local config_yaml = {
+    path = profile:yaml_path("apisix"),
+    type = "yaml",
+    parse = function(self)
+        local f, err = io.open(self.path, "r")
+        if not f then
+            return nil, "failed to open file " .. self.path .. " : " .. err
+        end
+
+        f:seek('end', -10)
+        local end_flag = f:read("*a")
+        local found_end_flag = re_find(end_flag, [[#END\s*$]], "jo")
+
+        if not found_end_flag then
+            f:close()
+            return nil, "missing valid end flag in file " .. self.path
+        end
+
+        f:seek('set')
+        local raw_config = f:read("*a")
+        f:close()
+
+        return yaml.load(raw_config), nil
+    end
+}
+
+local config_json = {
+    -- `-5` to remove the "yaml" suffix
+    path = config_yaml.path:sub(1, -5) .. "json",
+    type = "json",
+    parse = function(self)
+        local f, err = io.open(self.path, "r")
+        if not f then
+            return nil, "failed to open file " .. self.path .. " : " .. err
+        end
+        local raw_config = f:read("*a")
+        f:close()
+
+        local config, err = json.decode(raw_config)
+        if err then
+            return nil, "failed to decode json: " .. err
+        end
+        return config, nil
+    end
+}
+
+local config_file_table = {
+    yaml = config_yaml,
+    json = config_json
+}
+
+
+local config_file = setmetatable({}, {
+    __index = function(_, key)
+        return config_file_table[_M.file_type][key]
+    end
+})
+
+
 local function sync_status_to_shdict(status)
     if process.type() ~= "worker" then
         return
@@ -112,13 +172,13 @@ local function is_use_admin_api()
 end
 
 
-local function read_apisix_yaml(premature, pre_mtime)
+local function read_apisix_config(premature, pre_mtime)
     if premature then
         return
     end
-    local attributes, err = lfs.attributes(apisix_yaml_path)
+    local attributes, err = lfs.attributes(config_file.path)
     if not attributes then
-        log.error("failed to fetch ", apisix_yaml_path, " attributes: ", err)
+        log.error("failed to fetch ", config_file.path, " attributes: ", err)
         return
     end
 
@@ -127,36 +187,15 @@ local function read_apisix_yaml(premature, pre_mtime)
         return
     end
 
-    local f, err = io.open(apisix_yaml_path, "r")
-    if not f then
-        log.error("failed to open file ", apisix_yaml_path, " : ", err)
-        return
-    end
-
-    f:seek('end', -10)
-    local end_flag = f:read("*a")
-    -- log.info("flag: ", end_flag)
-    local found_end_flag = re_find(end_flag, [[#END\s*$]], "jo")
-
-    if not found_end_flag then
-        f:close()
-        log.warn("missing valid end flag in file ", apisix_yaml_path)
-        return
-    end
-
-    f:seek('set')
-    local yaml_config = f:read("*a")
-    f:close()
-
-    local apisix_yaml_new = yaml.load(yaml_config)
-    if not apisix_yaml_new then
-        log.error("failed to parse the content of file " .. apisix_yaml_path)
+    local config_new, err = config_file:parse()
+    if err then
+        log.error("failed to parse the content of file ", config_file.path, ": 
", err)
         return
     end
 
-    update_config(apisix_yaml_new, last_modification_time)
+    update_config(config_new, last_modification_time)
 
-    log.warn("config file ", apisix_yaml_path, " reloaded.")
+    log.warn("config file ", config_file.path, " reloaded.")
 end
 
 
@@ -171,7 +210,7 @@ local function sync_data(self)
     else
         if not apisix_yaml_mtime then
             log.warn("wait for more time")
-            return nil, "failed to read local file " .. apisix_yaml_path
+            return nil, "failed to read local file " .. config_file.path
         end
         conf_version = apisix_yaml_mtime
     end
@@ -395,7 +434,7 @@ local function _automatic_fetch(premature, self)
         local ok, ok2, err = pcall(sync_data, self)
         if not ok then
             err = ok2
-            log.error("failed to fetch data from local file " .. 
apisix_yaml_path .. ": ",
+            log.error("failed to fetch data from local file " .. 
config_file.path .. ": ",
                       err, ", ", tostring(self))
             ngx_sleep(3)
             break
@@ -403,7 +442,7 @@ local function _automatic_fetch(premature, self)
         elseif not ok2 and err then
             if err ~= "timeout" and err ~= "Key not found"
                and self.last_err ~= err then
-                log.error("failed to fetch data from local file " .. 
apisix_yaml_path .. ": ",
+                log.error("failed to fetch data from local file " .. 
config_file.path .. ": ",
                           err, ", ", tostring(self))
             end
 
@@ -477,7 +516,7 @@ function _M.new(key, opts)
         end
 
         if err then
-            log.error("failed to fetch data from local file ", 
apisix_yaml_path, ": ",
+            log.error("failed to fetch data from local file ", 
config_file.path, ": ",
                       err, ", ", key)
         end
 
@@ -517,7 +556,7 @@ function _M.init()
         return true
     end
 
-    read_apisix_yaml()
+    read_apisix_config()
     return true
 end
 
@@ -531,7 +570,7 @@ function _M.init_worker()
     end
 
     -- sync data in each non-master process
-    ngx.timer.every(1, read_apisix_yaml)
+    ngx.timer.every(1, read_apisix_config)
 
     return true
 end
diff --git a/docs/en/latest/deployment-modes.md 
b/docs/en/latest/deployment-modes.md
index 4e916ae14..3752858c0 100644
--- a/docs/en/latest/deployment-modes.md
+++ b/docs/en/latest/deployment-modes.md
@@ -6,6 +6,10 @@ keywords:
   - APISIX deployment modes
 description: Documentation about the three deployment modes of Apache APISIX.
 ---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
 <!--
 #
 # Licensed to the Apache Software Foundation (ASF) under one or more
@@ -31,7 +35,7 @@ APISIX has three different deployment modes for different 
production use cases.
 
|-----------------|----------------------------|---------------------------------------------------------------------------------------------------------------------|
 | traditional     | traditional                | Data plane and control plane 
are deployed together. `enable_admin` attribute should be disabled manually.    
       |
 | decoupled       | data_plane / control_plane | Data plane and control plane 
are deployed independently.                                                     
       |
-| standalone      | data_plane / traditional   | The `data_plane` mode loads 
configuration from a local YAML file, while the traditional mode expects 
configuration through Admin API.   |
+| standalone      | data_plane / traditional   | The `data_plane` mode loads 
configuration from a local YAML / JSON file, while the traditional mode expects 
configuration through Admin API.   |
 
 Each of these deployment modes are explained in detail below.
 
@@ -134,6 +138,17 @@ deployment:
     config_provider: yaml
 ```
 
+You can also provide the configuration in JSON format by placing it in 
`conf/apisix.json`. Before proceeding, you should change the 
`deployment.role_data_plane.config_provider` to `json`.
+
+Refer to the example below:
+
+```yaml
+deployment:
+  role: data_plane
+  role_data_plane:
+    config_provider: json
+```
+
 This makes it possible to disable the Admin API and discover configuration 
changes and reloads based on the local file system.
 
 #### API-driven (Experimental)
@@ -275,6 +290,8 @@ The API accepts input in the same format as the file-based 
mode, supporting both
 
 ### How to configure rules
 
+#### To `config_provider: yaml`
+
 All of the rules are stored in one file which named `conf/apisix.yaml`,
 APISIX checks if this file has any change **every second**.
 If the file is changed & it ends with `#END`,
@@ -312,10 +329,40 @@ routes:
 
 More information about using environment variables can be found 
[here](./admin-api.md#using-environment-variables).
 
+#### To `config_provider: json`
+
+All of the rules are stored in one file which named `conf/apisix.json`,
+APISIX checks if this file has any change **every second**.
+If the file is changed,
+APISIX loads the rules from this file and updates its memory.
+
+Here is a mini example:
+
+```json
+{
+  "routes": [
+    {
+      "uri": "/hello",
+      "upstream": {
+        "nodes": {
+          "127.0.0.1:1980": 1
+        },
+        "type": "roundrobin"
+      }
+    }
+  ]
+}
+```
+
+*WARNING*: when using `conf/apisix.json`, the `#END` marker is not required, 
as APISIX can directly parse and validate the JSON structure.
+
 ### How to configure Route
 
 Single Route:
 
+<Tabs>
+<TabItem value="yaml" label="YAML" default>
+
 ```yaml
 routes:
   -
@@ -327,8 +374,34 @@ routes:
 #END
 ```
 
+</TabItem>
+
+<TabItem value="json" label="JSON">
+
+```json
+{
+  "routes": [
+    {
+      "uri": "/hello",
+      "upstream": {
+        "nodes": {
+          "127.0.0.1:1980": 1
+        },
+        "type": "roundrobin"
+      }
+    }
+  ]
+}
+```
+
+</TabItem>
+</Tabs>
+
 Multiple Routes:
 
+<Tabs>
+<TabItem value="yaml" label="YAML" default>
+
 ```yaml
 routes:
   -
@@ -346,9 +419,44 @@ routes:
 #END
 ```
 
+</TabItem>
+
+<TabItem value="json" label="JSON">
+
+```json
+{
+  "routes": [
+    {
+      "uri": "/hello",
+      "upstream": {
+        "nodes": {
+          "127.0.0.1:1980": 1
+        },
+        "type": "roundrobin"
+      }
+    },
+    {
+      "uri": "/hello2",
+      "upstream": {
+        "nodes": {
+          "127.0.0.1:1981": 1
+        },
+        "type": "roundrobin"
+      }
+    }
+  ]
+}
+```
+
+</TabItem>
+</Tabs>
+
 ### How to configure Route + Service
 
-```yml
+<Tabs>
+<TabItem value="yaml" label="YAML" default>
+
+```yaml
 routes:
     -
         uri: /hello
@@ -363,9 +471,41 @@ services:
 #END
 ```
 
+</TabItem>
+
+<TabItem value="json" label="JSON">
+
+```json
+{
+  "routes": [
+    {
+      "uri": "/hello",
+      "service_id": 1
+    }
+  ],
+  "services": [
+    {
+      "id": 1,
+      "upstream": {
+        "nodes": {
+          "127.0.0.1:1980": 1
+        },
+        "type": "roundrobin"
+      }
+    }
+  ]
+}
+```
+
+</TabItem>
+</Tabs>
+
 ### How to configure Route + Upstream
 
-```yml
+<Tabs>
+<TabItem value="yaml" label="YAML" default>
+
+```yaml
 routes:
     -
         uri: /hello
@@ -379,9 +519,39 @@ upstreams:
 #END
 ```
 
+</TabItem>
+
+<TabItem value="json" label="JSON">
+
+```json
+{
+  "routes": [
+    {
+      "uri": "/hello",
+      "upstream_id": 1
+    }
+  ],
+  "upstreams": [
+    {
+      "id": 1,
+      "nodes": {
+        "127.0.0.1:1980": 1
+      },
+      "type": "roundrobin"
+    }
+  ]
+}
+```
+
+</TabItem>
+</Tabs>
+
 ### How to configure Route + Service + Upstream
 
-```yml
+<Tabs>
+<TabItem value="yaml" label="YAML" default>
+
+```yaml
 routes:
     -
         uri: /hello
@@ -399,9 +569,45 @@ upstreams:
 #END
 ```
 
+</TabItem>
+
+<TabItem value="json" label="JSON">
+
+```json
+{
+  "routes": [
+    {
+      "uri": "/hello",
+      "service_id": 1
+    }
+  ],
+  "services": [
+    {
+      "id": 1,
+      "upstream_id": 2
+    }
+  ],
+  "upstreams": [
+    {
+      "id": 2,
+      "nodes": {
+        "127.0.0.1:1980": 1
+      },
+      "type": "roundrobin"
+    }
+  ]
+}
+```
+
+</TabItem>
+</Tabs>
+
 ### How to configure Plugins
 
-```yml
+<Tabs>
+<TabItem value="yaml" label="YAML" default>
+
+```yaml
 # plugins listed here will be hot reloaded and override the boot configuration
 plugins:
   - name: ip-restriction
@@ -411,9 +617,36 @@ plugins:
 #END
 ```
 
+</TabItem>
+
+<TabItem value="json" label="JSON">
+
+```json
+{
+  "plugins": [
+    {
+      "name": "ip-restriction"
+    },
+    {
+      "name": "jwt-auth"
+    },
+    {
+      "name": "mqtt-proxy",
+      "stream": true
+    }
+  ]
+}
+```
+
+</TabItem>
+</Tabs>
+
 ### How to configure Plugin Configs
 
-```yml
+<Tabs>
+<TabItem value="yaml" label="YAML" default>
+
+```yaml
 plugin_configs:
     -
         id: 1
@@ -431,9 +664,47 @@ routes:
 #END
 ```
 
+</TabItem>
+
+<TabItem value="json" label="JSON">
+
+```json
+{
+  "plugin_configs": [
+    {
+      "id": 1,
+      "plugins": {
+        "response-rewrite": {
+          "body": "hello\n"
+        }
+      }
+    }
+  ],
+  "routes": [
+    {
+      "id": 1,
+      "uri": "/hello",
+      "plugin_config_id": 1,
+      "upstream": {
+        "nodes": {
+          "127.0.0.1:1980": 1
+        },
+        "type": "roundrobin"
+      }
+    }
+  ]
+}
+```
+
+</TabItem>
+</Tabs>
+
 ### How to enable SSL
 
-```yml
+<Tabs>
+<TabItem value="yaml" label="YAML" default>
+
+```yaml
 ssls:
     -
         cert: |
@@ -442,7 +713,7 @@ ssls:
             BAYTAkNOMREwDwYDVQQIDAhaaGVqaWFuZzERMA8GA1UEBwwISGFuZ3pob3UxDTAL
             BgNVBAoMBHRlc3QxDTALBgNVBAsMBHRlc3QxGzAZBgNVBAMMEmV0Y2QuY2x1c3Rl
             ci5sb2NhbDAeFw0yMDEwMjgwMzMzMDJaFw0yMTEwMjgwMzMzMDJaMG4xCzAJBgNV
-            BAYTAkNOMREwDwYDVQQIDAhaaGVqaWFuZzERMA8GA1UEBwwISGFuZ3pob3UxDTAL
+            BAYTAkNOMREwDwYDVQQIDAhaaGVqaWFuZzERMA0GA1UEBwwISGFuZ3pob3UxDTAL
             BgNVBAoMBHRlc3QxDTALBgNVBAsMBHRlc3QxGzAZBgNVBAMMEmV0Y2QuY2x1c3Rl
             ci5sb2NhbDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJ/qwxCR7g5S
             s9+VleopkLi5pAszEkHYOBpwF/hDeRdxU0I0e1zZTdTlwwPy2vf8m3kwoq6fmNCt
@@ -493,8 +764,32 @@ ssls:
 #END
 ```
 
+</TabItem>
+
+<TabItem value="json" label="JSON">
+
+```json
+{
+  "ssls": [
+    {
+      "cert": "-----BEGIN 
CERTIFICATE-----\nMIIDrzCCApegAwIBAgIJAI3Meu/gJVTLMA0GCSqGSIb3DQEBCwUAMG4xCzAJBgNV\nBAYTAkNOMREwDwYDVQQIDAhaaGVqaWFuZzERMA8GA1UEBwwISGFuZ3pob3UxDTAL\nBgNVBAoMBHRlc3QxDTALBgNVBAsMBHRlc3QxGzAZBgNVBAMMEmV0Y2QuY2x1c3Rl\nci5sb2NhbDAeFw0yMDEwMjgwMzMzMDJaFw0yMTEwMjgwMzMzMDJaMG4xCzAJBgNV\nBAYTAkNOMREwDwYDVQQIDAhaaGVqaWFuZzERMA8GA1UEBwwISGFuZ3pob3UxDTAL\nBgNVBAoMBHRlc3QxDTALBgNVBAsMBHRlc3QxGzAZBgNVBAMMEmV0Y2QuY2x1c3Rl\nci5sb2NhbDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
 [...]
+      "key": "-----BEGIN PRIVATE 
KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCf6sMQke4OUrPf\nlZXqKZC4uaQLMxJB2DgacBf4Q3kXcVNCNHtc2U3U5cMD8tr3/Jt5MKKun5jQrbXV\nFF4eVr4Iv9jgPBwQc2kBUC9QL/alsMhEuXMeqdGQcCK3F0CLJdV3zUlKUDU0kg+O\nExnbl1CHXrIbpD7zLy1i3s8p39v1pYFYf4WlrQxvfa/xo97gXY5dJv8RryryLzRc\nuhHYBvX5MHCGpbrY61JxpfZqBo8CmLuHl1tmbeXpdHdQB11LKiuL6HtKflNjc6rg\n5r8bXl1nZbM/KOZEE+muA1LVoaTyHzY/aGXz0bNy4QRUO+De9JFcTDgnXnNZVG5x\ncyyDBpc9AgMBAAECggEAatcEtehZPJaCeClPPF/Cwbe9YoIfe4BCk
 [...]
+      "snis": [
+        "yourdomain.com"
+      ]
+    }
+  ]
+}
+```
+
+</TabItem>
+</Tabs>
+
 ### How to configure global rule
 
+<Tabs>
+<TabItem value="yaml" label="YAML" default>
+
 ```yaml
 global_rules:
     -
@@ -505,8 +800,33 @@ global_rules:
 #END
 ```
 
+</TabItem>
+
+<TabItem value="json" label="JSON">
+
+```json
+{
+  "global_rules": [
+    {
+      "id": 1,
+      "plugins": {
+        "response-rewrite": {
+          "body": "hello\n"
+        }
+      }
+    }
+  ]
+}
+```
+
+</TabItem>
+</Tabs>
+
 ### How to configure consumer
 
+<Tabs>
+<TabItem value="yaml" label="YAML" default>
+
 ```yaml
 consumers:
   - username: jwt
@@ -517,8 +837,34 @@ consumers:
 #END
 ```
 
+</TabItem>
+
+<TabItem value="json" label="JSON">
+
+```json
+{
+  "consumers": [
+    {
+      "username": "jwt",
+      "plugins": {
+        "jwt-auth": {
+          "key": "user-key",
+          "secret": "my-secret-key"
+        }
+      }
+    }
+  ]
+}
+```
+
+</TabItem>
+</Tabs>
+
 ### How to configure plugin metadata
 
+<Tabs>
+<TabItem value="yaml" label="YAML" default>
+
 ```yaml
 upstreams:
   - id: 1
@@ -541,8 +887,53 @@ plugin_metadata:
 #END
 ```
 
+</TabItem>
+
+<TabItem value="json" label="JSON">
+
+```json
+{
+  "upstreams": [
+    {
+      "id": 1,
+      "nodes": {
+        "127.0.0.1:1980": 1
+      },
+      "type": "roundrobin"
+    }
+  ],
+  "routes": [
+    {
+      "uri": "/hello",
+      "upstream_id": 1,
+      "plugins": {
+        "http-logger": {
+          "batch_max_size": 1,
+          "uri": "http://127.0.0.1:1980/log";
+        }
+      }
+    }
+  ],
+  "plugin_metadata": [
+    {
+      "id": "http-logger",
+      "log_format": {
+        "host": "$host",
+        "remote_addr": "$remote_addr"
+      }
+    }
+  ]
+}
+```
+
+</TabItem>
+</Tabs>
+
 ### How to configure stream route
 
+<Tabs>
+<TabItem value="yaml" label="YAML" default>
+
 ```yaml
 stream_routes:
   - server_addr: 127.0.0.1
@@ -561,8 +952,46 @@ upstreams:
 #END
 ```
 
+</TabItem>
+
+<TabItem value="json" label="JSON">
+
+```json
+{
+  "stream_routes": [
+    {
+      "server_addr": "127.0.0.1",
+      "server_port": 1985,
+      "id": 1,
+      "upstream_id": 1,
+      "plugins": {
+        "mqtt-proxy": {
+          "protocol_name": "MQTT",
+          "protocol_level": 4
+        }
+      }
+    }
+  ],
+  "upstreams": [
+    {
+      "nodes": {
+        "127.0.0.1:1995": 1
+      },
+      "type": "roundrobin",
+      "id": 1
+    }
+  ]
+}
+```
+
+</TabItem>
+</Tabs>
+
 ### How to configure protos
 
+<Tabs>
+<TabItem value="yaml" label="YAML" default>
+
 ```yaml
 protos:
   - id: helloworld
@@ -582,3 +1011,22 @@ protos:
       }
 #END
 ```
+
+</TabItem>
+
+<TabItem value="json" label="JSON">
+
+```json
+{
+  "protos": [
+    {
+      "id": "helloworld",
+      "desc": "hello world",
+      "content": "syntax = \"proto3\";\npackage helloworld;\n\nservice Greeter 
{\n  rpc SayHello (HelloRequest) returns (HelloReply) {}\n}\nmessage 
HelloRequest {\n  string name = 1;\n}\nmessage HelloReply {\n  string message = 
1;\n}\n"
+    }
+  ]
+}
+```
+
+</TabItem>
+</Tabs>
diff --git a/t/APISIX.pm b/t/APISIX.pm
index 69f4b6fb1..f9d633880 100644
--- a/t/APISIX.pm
+++ b/t/APISIX.pm
@@ -125,14 +125,17 @@ my $profile = $ENV{"APISIX_PROFILE"};
 
 
 my $apisix_file;
+my $apisix_file_json;
 my $debug_file;
 my $config_file;
 if ($profile) {
     $apisix_file = "apisix-$profile.yaml";
+    $apisix_file_json = "apisix-$profile.json";
     $debug_file = "debug-$profile.yaml";
     $config_file = "config-$profile.yaml";
 } else {
     $apisix_file = "apisix.yaml";
+    $apisix_file_json = "apisix.json";
     $debug_file = "debug.yaml";
     $config_file = "config.yaml";
 }
@@ -254,6 +257,18 @@ deployment:
 _EOC_
     }
 
+    if ($block->apisix_json && (!defined $block->yaml_config)) {
+        $user_yaml_config = <<_EOC_;
+apisix:
+    node_listen: 1984
+    enable_admin: false
+deployment:
+    role: data_plane
+    role_data_plane:
+        config_provider: json
+_EOC_
+    }
+
     my $lua_deps_path = $block->lua_deps_path // <<_EOC_;
     lua_package_path 
"$apisix_home/?.lua;$apisix_home/?/init.lua;$apisix_home/deps/share/lua/5.1/?/init.lua;$apisix_home/deps/share/lua/5.1/?.lua;$apisix_home/apisix/?.lua;$apisix_home/t/?.lua;$apisix_home/t/xrpc/?.lua;$apisix_home/t/xrpc/?/init.lua;;";
     lua_package_cpath 
"$apisix_home/?.so;$apisix_home/deps/lib/lua/5.1/?.so;$apisix_home/deps/lib64/lua/5.1/?.so;;";
@@ -915,6 +930,14 @@ $user_apisix_yaml
 _EOC_
     }
 
+    my $user_apisix_json = $block->apisix_json // "";
+    if ($user_apisix_json){
+        $user_apisix_json = <<_EOC_;
+>>> ../conf/$apisix_file_json
+$user_apisix_json
+_EOC_
+    }
+
     my $yaml_config = $block->yaml_config // $user_yaml_config;
 
     my $default_deployment = <<_EOC_;
@@ -959,6 +982,7 @@ $etcd_pem
 >>> ../conf/cert/etcd.key
 $etcd_key
 $user_apisix_yaml
+$user_apisix_json
 _EOC_
 
     $block->set_value("user_files", $user_files);
diff --git a/t/config-center-json/consumer-group.t 
b/t/config-center-json/consumer-group.t
new file mode 100644
index 000000000..329380059
--- /dev/null
+++ b/t/config-center-json/consumer-group.t
@@ -0,0 +1,212 @@
+#
+# 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';
+
+repeat_each(1);
+log_level('info');
+no_root_location();
+no_shuffle();
+
+add_block_preprocessor(sub {
+    my ($block) = @_;
+
+    if (!$block->request) {
+        $block->set_value("request", "GET /hello?apikey=one");
+    }
+
+    if ((!defined $block->error_log) && (!defined $block->no_error_log)) {
+        $block->set_value("no_error_log", "[error]");
+    }
+});
+
+
+run_tests();
+
+__DATA__
+
+=== TEST 1: sanity
+--- apisix_json
+{
+  "routes": [
+    {
+      "uri": "/hello",
+      "plugins": {
+        "key-auth": {}
+      },
+      "upstream": {
+        "nodes": {
+          "127.0.0.1:1980": 1
+        },
+        "type": "roundrobin"
+      }
+    }
+  ],
+  "consumer_groups": [
+    {
+      "id": "foobar",
+      "plugins": {
+        "response-rewrite": {
+          "body": "hello\n"
+        }
+      }
+    }
+  ],
+  "consumers": [
+    {
+      "username": "one",
+      "group_id": "foobar",
+      "plugins": {
+        "key-auth": {
+          "key": "one"
+        }
+      }
+    }
+  ]
+}
+--- response_body
+hello
+
+
+
+=== TEST 2: consumer group not found
+--- apisix_json
+{
+  "routes": [
+    {
+      "uri": "/hello",
+      "plugins": {
+        "key-auth": {}
+      },
+      "upstream": {
+        "nodes": {
+          "127.0.0.1:1980": 1
+        },
+        "type": "roundrobin"
+      }
+    }
+  ],
+  "consumers": [
+    {
+      "username": "one",
+      "group_id": "invalid_group",
+      "plugins": {
+        "key-auth": {
+          "key": "one"
+        }
+      }
+    }
+  ]
+}
+--- error_code: 503
+--- error_log
+failed to fetch consumer group config by id: invalid_group
+
+
+
+=== TEST 3: plugin priority
+--- apisix_json
+{
+  "routes": [
+    {
+      "uri": "/hello",
+      "plugins": {
+        "key-auth": {}
+      },
+      "upstream": {
+        "nodes": {
+          "127.0.0.1:1980": 1
+        },
+        "type": "roundrobin"
+      }
+    }
+  ],
+  "consumer_groups": [
+    {
+      "id": "foobar",
+      "plugins": {
+        "response-rewrite": {
+          "body": "hello\n"
+        }
+      }
+    }
+  ],
+  "consumers": [
+    {
+      "username": "one",
+      "group_id": "foobar",
+      "plugins": {
+        "key-auth": {
+          "key": "one"
+        },
+        "response-rewrite": {
+          "body": "world\n"
+        }
+      }
+    }
+  ]
+}
+--- response_body
+world
+
+
+
+=== TEST 4: invalid plugin
+--- apisix_json
+{
+  "routes": [
+    {
+      "uri": "/hello",
+      "plugins": {
+        "key-auth": {}
+      },
+      "upstream": {
+        "nodes": {
+          "127.0.0.1:1980": 1
+        },
+        "type": "roundrobin"
+      }
+    }
+  ],
+  "consumer_groups": [
+    {
+      "id": "foobar",
+      "plugins": {
+        "example-plugin": {
+          "skey": "s"
+        },
+        "response-rewrite": {
+          "body": "hello\n"
+        }
+      }
+    }
+  ],
+  "consumers": [
+    {
+      "username": "one",
+      "group_id": "foobar",
+      "plugins": {
+        "key-auth": {
+          "key": "one"
+        }
+      }
+    }
+  ]
+}
+--- error_code: 503
+--- error_log
+failed to check the configuration of plugin example-plugin
+failed to fetch consumer group config by id: foobar
diff --git a/t/config-center-json/consumer.t b/t/config-center-json/consumer.t
new file mode 100644
index 000000000..80b801982
--- /dev/null
+++ b/t/config-center-json/consumer.t
@@ -0,0 +1,93 @@
+#
+# 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';
+
+repeat_each(1);
+log_level('info');
+no_root_location();
+no_shuffle();
+
+run_tests();
+
+__DATA__
+
+=== TEST 1: validate consumer
+--- apisix_json
+{
+  "routes": [
+    {
+      "uri": "/hello",
+      "upstream": {
+        "nodes": {
+          "127.0.0.1:1980": 1
+        },
+        "type": "roundrobin"
+      }
+    }
+  ],
+  "consumers": [
+    {
+      "username": "jwt#auth"
+    }
+  ]
+}
+--- request
+GET /hello
+--- response_body
+hello world
+--- error_log
+property "username" validation failed
+
+
+
+=== TEST 2: consumer restriction
+--- apisix_json
+{
+  "consumers": [
+    {
+      "username": "jack",
+      "plugins": {
+        "key-auth": {
+          "key": "user-key"
+        }
+      }
+    }
+  ],
+  "routes": [
+    {
+      "id": "1",
+      "methods": ["POST"],
+      "uri": "/hello",
+      "plugins": {
+        "key-auth": {},
+        "consumer-restriction": {
+          "whitelist": ["jack"]
+        }
+      },
+      "upstream": {
+        "type": "roundrobin",
+        "nodes": {
+          "127.0.0.1:1980": 1
+        }
+      }
+    }
+  ]
+}
+--- more_headers
+apikey: user-key
+--- request
+POST /hello
diff --git a/t/config-center-json/global-rule.t 
b/t/config-center-json/global-rule.t
new file mode 100644
index 000000000..54d589827
--- /dev/null
+++ b/t/config-center-json/global-rule.t
@@ -0,0 +1,160 @@
+#
+# 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';
+
+repeat_each(1);
+log_level('info');
+no_root_location();
+no_shuffle();
+
+add_block_preprocessor(sub {
+    my ($block) = @_;
+
+    if (!$block->request) {
+        $block->set_value("request", "GET /hello");
+    }
+
+    if (!$block->error_log && !$block->no_error_log) {
+        $block->set_value("no_error_log", "[error]\n[alert]");
+    }
+});
+
+run_tests();
+
+__DATA__
+
+=== TEST 1: sanity
+--- apisix_json
+{
+  "routes": [
+    {
+      "id": 1,
+      "uri": "/hello",
+      "upstream": {
+        "nodes": {
+          "127.0.0.1:1980": 1
+        },
+        "type": "roundrobin"
+      }
+    }
+  ],
+  "global_rules": [
+    {
+      "id": 1,
+      "plugins": {
+        "response-rewrite": {
+          "body": "hello\n"
+        }
+      }
+    }
+  ]
+}
+--- response_body
+hello
+
+
+
+=== TEST 2: global rule with bad plugin
+--- apisix_json
+{
+  "routes": [
+    {
+      "id": 1,
+      "uri": "/hello",
+      "upstream": {
+        "nodes": {
+          "127.0.0.1:1980": 1
+        },
+        "type": "roundrobin"
+      }
+    }
+  ],
+  "global_rules": [
+    {
+      "id": 1,
+      "plugins": {
+        "response-rewrite": {
+          "body": 4
+        }
+      }
+    }
+  ]
+}
+--- response_body
+hello world
+--- error_log
+property "body" validation failed
+
+
+
+=== TEST 3: fix global rule with default value
+--- apisix_json
+{
+  "routes": [
+    {
+      "id": 1,
+      "uri": "/hello",
+      "upstream": {
+        "nodes": {
+          "127.0.0.1:1980": 1
+        },
+        "type": "roundrobin"
+      }
+    }
+  ],
+  "global_rules": [
+    {
+      "id": 1,
+      "plugins": {
+        "uri-blocker": {
+          "block_rules": [
+            "/h*"
+          ]
+        }
+      }
+    }
+  ]
+}
+--- error_code: 403
+
+
+
+=== TEST 4: common phase without matched route
+--- apisix_json
+{
+  "routes": [
+    {
+      "uri": "/apisix/prometheus/metrics",
+      "plugins": {
+        "public-api": {}
+      }
+    }
+  ],
+  "global_rules": [
+    {
+      "id": 1,
+      "plugins": {
+        "cors": {
+          "allow_origins": "http://a.com,http://b.com";
+        }
+      }
+    }
+  ]
+}
+--- request
+GET /apisix/prometheus/metrics
+--- error_code: 200
diff --git a/t/config-center-json/plugin-configs.t 
b/t/config-center-json/plugin-configs.t
new file mode 100644
index 000000000..79f8d0f03
--- /dev/null
+++ b/t/config-center-json/plugin-configs.t
@@ -0,0 +1,175 @@
+#
+# 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';
+
+repeat_each(1);
+log_level('info');
+no_root_location();
+no_shuffle();
+
+add_block_preprocessor(sub {
+    my ($block) = @_;
+
+    if (!$block->request) {
+        $block->set_value("request", "GET /hello");
+    }
+});
+
+run_tests();
+
+__DATA__
+
+=== TEST 1: sanity
+--- apisix_json
+{
+  "plugin_configs": [
+    {
+      "id": 1,
+      "plugins": {
+        "response-rewrite": {
+          "body": "hello\n"
+        }
+      }
+    }
+  ],
+  "routes": [
+    {
+      "id": 1,
+      "uri": "/hello",
+      "plugin_config_id": 1,
+      "upstream": {
+        "nodes": {
+          "127.0.0.1:1980": 1
+        },
+        "type": "roundrobin"
+      }
+    }
+  ]
+}
+--- response_body
+hello
+
+
+
+=== TEST 2: plugin_config not found
+--- apisix_json
+{
+  "routes": [
+    {
+      "id": 1,
+      "uri": "/hello",
+      "plugin_config_id": 1,
+      "upstream": {
+        "nodes": {
+          "127.0.0.1:1980": 1
+        },
+        "type": "roundrobin"
+      }
+    }
+  ]
+}
+--- error_code: 503
+--- error_log
+failed to fetch plugin config by id: 1
+
+
+
+=== TEST 3: mix plugins & plugin_config_id
+--- apisix_json
+{
+  "plugin_configs": [
+    {
+      "id": 1,
+      "plugins": {
+        "example-plugin": {
+          "i": 1
+        },
+        "response-rewrite": {
+          "body": "hello\n"
+        }
+      }
+    }
+  ],
+  "routes": [
+    {
+      "id": 1,
+      "uri": "/echo",
+      "plugin_config_id": 1,
+      "plugins": {
+        "proxy-rewrite": {
+          "headers": {
+            "in": "out"
+          }
+        },
+        "response-rewrite": {
+          "body": "world\n"
+        }
+      },
+      "upstream": {
+        "nodes": {
+          "127.0.0.1:1980": 1
+        },
+        "type": "roundrobin"
+      }
+    }
+  ]
+}
+--- request
+GET /echo
+--- response_body
+world
+--- response_headers
+in: out
+--- error_log eval
+qr/conf_version: \d+#\d+,/
+
+
+
+=== TEST 4: invalid plugin
+--- apisix_json
+{
+  "plugin_configs": [
+    {
+      "id": 1,
+      "plugins": {
+        "example-plugin": {
+          "skey": "s"
+        },
+        "response-rewrite": {
+          "body": "hello\n"
+        }
+      }
+    }
+  ],
+  "routes": [
+    {
+      "id": 1,
+      "uri": "/hello",
+      "plugin_config_id": 1,
+      "upstream": {
+        "nodes": {
+          "127.0.0.1:1980": 1
+        },
+        "type": "roundrobin"
+      }
+    }
+  ]
+}
+--- error_code: 503
+--- error_log
+failed to check the configuration of plugin example-plugin
+failed to fetch plugin config by id: 1
diff --git a/t/config-center-json/plugin-metadata.t 
b/t/config-center-json/plugin-metadata.t
new file mode 100644
index 000000000..2eed9d418
--- /dev/null
+++ b/t/config-center-json/plugin-metadata.t
@@ -0,0 +1,100 @@
+#
+# 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';
+
+repeat_each(1);
+log_level('info');
+no_root_location();
+no_shuffle();
+
+
+run_tests();
+
+__DATA__
+
+=== TEST 1: sanity
+--- apisix_json
+{
+  "upstreams": [
+    {
+      "id": 1,
+      "nodes": {
+        "127.0.0.1:1980": 1
+      },
+      "type": "roundrobin"
+    }
+  ],
+  "routes": [
+    {
+      "uri": "/hello",
+      "upstream_id": 1,
+      "plugins": {
+        "http-logger": {
+          "batch_max_size": 1,
+          "uri": "http://127.0.0.1:1980/log";
+        }
+      }
+    }
+  ],
+  "plugin_metadata": [
+    {
+      "id": "http-logger",
+      "log_format": {
+        "host": "$host",
+        "remote_addr": "$remote_addr"
+      }
+    }
+  ]
+}
+--- request
+GET /hello
+--- error_log
+"remote_addr":"127.0.0.1"
+--- no_error_log
+failed to get schema for plugin:
+
+
+
+=== TEST 2: sanity
+--- apisix_json
+{
+  "upstreams": [
+    {
+      "id": 1,
+      "nodes": {
+        "127.0.0.1:1980": 1
+      },
+      "type": "roundrobin"
+    }
+  ],
+  "routes": [
+    {
+      "uri": "/hello",
+      "upstream_id": 1
+    }
+  ],
+  "plugin_metadata": [
+    {
+      "id": "authz-casbin",
+      "model": 123
+    }
+  ]
+}
+--- request
+GET /hello
+--- error_log
+failed to check item data of [plugin_metadata]
diff --git a/t/config-center-json/plugin.t b/t/config-center-json/plugin.t
new file mode 100644
index 000000000..9dd143700
--- /dev/null
+++ b/t/config-center-json/plugin.t
@@ -0,0 +1,291 @@
+#
+# 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';
+
+repeat_each(1);
+log_level('info');
+no_root_location();
+no_shuffle();
+
+our $debug_config = t::APISIX::read_file("conf/debug.yaml");
+$debug_config =~ s/basic:\n  enable: false/basic:\n  enable: true/;
+
+run_tests();
+
+__DATA__
+
+=== TEST 1: sanity
+--- apisix_json
+{
+  "routes": [
+    {
+      "uri": "/hello",
+      "upstream": {
+        "nodes": {
+          "127.0.0.1:1980": 1
+        },
+        "type": "roundrobin"
+      }
+    }
+  ],
+  "plugins": [
+    {
+      "name": "ip-restriction"
+    },
+    {
+      "name": "jwt-auth"
+    },
+    {
+      "name": "mqtt-proxy",
+      "stream": true
+    }
+  ]
+}
+--- debug_config eval: $::debug_config
+--- config
+    location /t {
+        content_by_lua_block {
+            ngx.sleep(0.3)
+            local http = require "resty.http"
+            local httpc = http.new()
+            local uri = "http://127.0.0.1:"; .. ngx.var.server_port .. "/hello"
+            local res, err = httpc:request_uri(uri, {
+                    method = "GET",
+                })
+            ngx.print(res.body)
+        }
+    }
+--- request
+GET /t
+--- response_body
+hello world
+--- error_log
+use config_provider: yaml
+load(): loaded plugin and sort by priority: 3000 name: ip-restriction
+load(): loaded plugin and sort by priority: 2510 name: jwt-auth
+load_stream(): loaded stream plugin and sort by priority: 1000 name: mqtt-proxy
+--- grep_error_log eval
+qr/load\(\): new plugins/
+--- grep_error_log_out
+load(): new plugins
+load(): new plugins
+load(): new plugins
+load(): new plugins
+
+
+
+=== TEST 2: plugins not changed, but still need to reload
+--- yaml_config
+apisix:
+    node_listen: 1984
+    enable_admin: false
+deployment:
+    role: data_plane
+    role_data_plane:
+        config_provider: yaml
+plugins:
+    - ip-restriction
+    - jwt-auth
+stream_plugins:
+    - mqtt-proxy
+--- apisix_json
+{
+  "routes": [
+    {
+      "uri": "/hello",
+      "upstream": {
+        "nodes": {
+          "127.0.0.1:1980": 1
+        },
+        "type": "roundrobin"
+      }
+    }
+  ],
+  "plugins": [
+    {
+      "name": "ip-restriction"
+    },
+    {
+      "name": "jwt-auth"
+    },
+    {
+      "name": "mqtt-proxy",
+      "stream": true
+    }
+  ]
+}
+--- debug_config eval: $::debug_config
+--- config
+    location /t {
+        content_by_lua_block {
+            ngx.sleep(0.3)
+            local http = require "resty.http"
+            local httpc = http.new()
+            local uri = "http://127.0.0.1:"; .. ngx.var.server_port .. "/hello"
+            local res, err = httpc:request_uri(uri, {
+                    method = "GET",
+                })
+            ngx.print(res.body)
+        }
+    }
+--- request
+GET /t
+--- response_body
+hello world
+--- grep_error_log eval
+qr/loaded plugin and sort by priority: \d+ name: [^,]+/
+--- grep_error_log_out eval
+qr/(loaded plugin and sort by priority: (3000 name: ip-restriction|2510 name: 
jwt-auth)
+){4}/
+
+
+
+=== TEST 3: disable plugin and its router
+--- apisix_json
+{
+  "routes": [
+    {
+      "uri": "/hello",
+      "upstream": {
+        "nodes": {
+          "127.0.0.1:1980": 1
+        },
+        "type": "roundrobin"
+      }
+    }
+  ],
+  "plugins": [
+    {
+      "name": "jwt-auth"
+    }
+  ]
+}
+--- request
+GET /apisix/prometheus/metrics
+--- error_code: 404
+
+
+
+=== TEST 4: enable plugin and its router
+--- apisix_json
+{
+  "routes": [
+    {
+      "uri": "/apisix/prometheus/metrics",
+      "plugins": {
+        "public-api": {}
+      }
+    }
+  ],
+  "plugins": [
+    {
+      "name": "public-api"
+    },
+    {
+      "name": "prometheus"
+    }
+  ]
+}
+--- request
+GET /apisix/prometheus/metrics
+
+
+
+=== TEST 5: invalid plugin config
+--- yaml_config
+apisix:
+    node_listen: 1984
+    enable_admin: false
+deployment:
+    role: data_plane
+    role_data_plane:
+        config_provider: yaml
+plugins:
+    - ip-restriction
+    - jwt-auth
+stream_plugins:
+    - mqtt-proxy
+--- apisix_json
+{
+  "routes": [
+    {
+      "uri": "/hello",
+      "upstream": {
+        "nodes": {
+          "127.0.0.1:1980": 1
+        },
+        "type": "roundrobin"
+      }
+    }
+  ],
+  "plugins": [
+    {
+      "name": "xxx",
+      "stream": "ip-restriction"
+    }
+  ]
+}
+--- request
+GET /hello
+--- response_body
+hello world
+--- error_log
+property "stream" validation failed: wrong type: expected boolean, got string
+--- no_error_log
+load(): plugins not changed
+
+
+
+=== TEST 6: empty plugin list
+--- apisix_json
+{
+  "routes": [
+    {
+      "uri": "/hello",
+      "upstream": {
+        "nodes": {
+          "127.0.0.1:1980": 1
+        },
+        "type": "roundrobin"
+      }
+    }
+  ],
+  "plugins": [],
+  "stream_plugins": []
+}
+--- debug_config eval: $::debug_config
+--- config
+    location /t {
+        content_by_lua_block {
+            ngx.sleep(0.3)
+            local http = require "resty.http"
+            local httpc = http.new()
+            local uri = "http://127.0.0.1:"; .. ngx.var.server_port .. "/hello"
+            local res, err = httpc:request_uri(uri, {
+                    method = "GET",
+                })
+            ngx.print(res.body)
+        }
+    }
+--- request
+GET /t
+--- response_body
+hello world
+--- error_log
+use config_provider: yaml
+load(): new plugins: {}
+load_stream(): new plugins: {}
diff --git a/t/config-center-json/route-service.t 
b/t/config-center-json/route-service.t
new file mode 100644
index 000000000..b16e68056
--- /dev/null
+++ b/t/config-center-json/route-service.t
@@ -0,0 +1,379 @@
+#
+# 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';
+
+repeat_each(1);
+log_level('info');
+no_root_location();
+no_shuffle();
+
+run_tests();
+
+__DATA__
+
+=== TEST 1: hit route
+--- yaml_config eval: $::yaml_config
+--- apisix_json
+{
+  "routes": [
+    {
+      "uri": "/hello",
+      "service_id": 1,
+      "id": 1
+    }
+  ],
+  "services": [
+    {
+      "id": 1,
+      "upstream": {
+        "nodes": {
+          "127.0.0.1:1980": 1
+        },
+        "type": "roundrobin"
+      }
+    }
+  ]
+}
+--- request
+GET /hello
+--- response_body
+hello world
+
+
+
+=== TEST 2: not found service
+--- yaml_config eval: $::yaml_config
+--- apisix_json
+{
+  "routes": [
+    {
+      "uri": "/hello",
+      "id": 1,
+      "service_id": 1111
+    }
+  ],
+  "services": [
+    {
+      "id": 1,
+      "upstream": {
+        "nodes": {
+          "127.0.0.1:1980": 1
+        },
+        "type": "roundrobin"
+      }
+    }
+  ]
+}
+--- request
+GET /hello
+--- error_code: 404
+--- error_log
+failed to fetch service configuration by id: 1111
+
+
+
+=== TEST 3: service upstream priority
+--- yaml_config eval: $::yaml_config
+--- apisix_json
+{
+  "routes": [
+    {
+      "id": 1,
+      "uri": "/hello",
+      "service_id": 1
+    }
+  ],
+  "services": [
+    {
+      "id": 1,
+      "upstream": {
+        "nodes": {
+          "127.0.0.1:1977": 1
+        },
+        "type": "roundrobin"
+      },
+      "upstream_id": 1
+    }
+  ],
+  "upstreams": [
+    {
+      "id": 1,
+      "nodes": {
+        "127.0.0.1:1980": 1
+      },
+      "type": "roundrobin"
+    }
+  ]
+}
+--- request
+GET /hello
+--- response_body
+hello world
+
+
+
+=== TEST 4: route service upstream priority
+--- yaml_config eval: $::yaml_config
+--- apisix_json
+{
+  "routes": [
+    {
+      "id": 1,
+      "uri": "/hello",
+      "service_id": 1,
+      "upstream": {
+        "nodes": {
+          "127.0.0.1:1980": 1
+        },
+        "type": "roundrobin"
+      }
+    }
+  ],
+  "services": [
+    {
+      "id": 1,
+      "upstream": {
+        "nodes": {
+          "127.0.0.1:1977": 1
+        },
+        "type": "roundrobin"
+      }
+    }
+  ],
+  "upstreams": [
+    {
+      "id": 1,
+      "nodes": {
+        "127.0.0.1:1977": 1
+      },
+      "type": "roundrobin"
+    }
+  ]
+}
+--- request
+GET /hello
+--- response_body
+hello world
+
+
+
+=== TEST 5: route service upstream by upstream_id priority
+--- yaml_config eval: $::yaml_config
+--- apisix_json
+{
+  "routes": [
+    {
+      "id": 1,
+      "uri": "/hello",
+      "service_id": 1,
+      "upstream": {
+        "nodes": {
+          "127.0.0.1:1977": 1
+        },
+        "type": "roundrobin"
+      },
+      "upstream_id": 1
+    }
+  ],
+  "services": [
+    {
+      "id": 1,
+      "upstream": {
+        "nodes": {
+          "127.0.0.1:1977": 1
+        },
+        "type": "roundrobin"
+      }
+    }
+  ],
+  "upstreams": [
+    {
+      "id": 1,
+      "nodes": {
+        "127.0.0.1:1980": 1
+      },
+      "type": "roundrobin"
+    }
+  ]
+}
+--- request
+GET /hello
+--- response_body
+hello world
+
+
+
+=== TEST 6: route service upstream priority
+--- yaml_config eval: $::yaml_config
+--- apisix_json
+{
+  "routes": [
+    {
+      "id": 1,
+      "uri": "/hello",
+      "service_id": 1,
+      "upstream": {
+        "nodes": {
+          "127.0.0.1:1980": 1
+        },
+        "type": "roundrobin"
+      }
+    }
+  ],
+  "services": [
+    {
+      "id": 1,
+      "upstream": {
+        "nodes": {
+          "127.0.0.1:1977": 1
+        },
+        "type": "roundrobin"
+      },
+      "upstream_id": 1
+    }
+  ],
+  "upstreams": [
+    {
+      "id": 1,
+      "nodes": {
+        "127.0.0.1:1977": 1
+      },
+      "type": "roundrobin"
+    }
+  ]
+}
+--- request
+GET /hello
+--- response_body
+hello world
+
+
+
+=== TEST 7: two routes with the same service
+--- yaml_config eval: $::yaml_config
+--- apisix_json
+{
+  "routes": [
+    {
+      "uris": ["/hello"],
+      "service_id": 1,
+      "id": 1,
+      "plugins": {
+        "response-rewrite": {
+          "body": "hello\n"
+        }
+      }
+    },
+    {
+      "uris": ["/world"],
+      "service_id": 1,
+      "id": 2,
+      "plugins": {
+        "response-rewrite": {
+          "body": "world\n"
+        }
+      }
+    }
+  ],
+  "services": [
+    {
+      "id": 1,
+      "upstream": {
+        "nodes": {
+          "127.0.0.1:1980": 1
+        },
+        "type": "roundrobin"
+      }
+    }
+  ]
+}
+--- request
+GET /hello
+--- response_body
+hello
+
+
+
+=== TEST 8: service with bad plugin
+--- yaml_config eval: $::yaml_config
+--- apisix_json
+{
+  "routes": [
+    {
+      "id": 1,
+      "uri": "/hello",
+      "service_id": 1
+    }
+  ],
+  "services": [
+    {
+      "id": 1,
+      "plugins": {
+        "proxy-rewrite": {
+          "uri": 1
+        }
+      },
+      "upstream": {
+        "nodes": {
+          "127.0.0.1:1980": 1
+        },
+        "type": "roundrobin"
+      }
+    }
+  ]
+}
+--- request
+GET /hello
+--- error_code: 404
+--- error_log
+property "uri" validation failed
+
+
+
+=== TEST 9: fix service with default value
+--- yaml_config eval: $::yaml_config
+--- apisix_json
+{
+  "routes": [
+    {
+      "id": 1,
+      "uri": "/hello",
+      "service_id": 1
+    }
+  ],
+  "services": [
+    {
+      "id": 1,
+      "plugins": {
+        "uri-blocker": {
+          "block_rules": [
+            "/h*"
+          ]
+        }
+      },
+      "upstream": {
+        "nodes": {
+          "127.0.0.1:1980": 1
+        },
+        "type": "roundrobin"
+      }
+    }
+  ]
+}
+--- request
+GET /hello
+--- error_code: 403
diff --git a/t/config-center-json/route-upstream.t 
b/t/config-center-json/route-upstream.t
new file mode 100644
index 000000000..7082de27c
--- /dev/null
+++ b/t/config-center-json/route-upstream.t
@@ -0,0 +1,244 @@
+#
+# 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';
+
+repeat_each(1);
+log_level('info');
+no_root_location();
+no_shuffle();
+
+run_tests();
+
+__DATA__
+
+=== TEST 1: hit route
+--- yaml_config eval: $::yaml_config
+--- apisix_json
+{
+  "routes": [
+    {
+      "id": 1,
+      "uri": "/hello",
+      "upstream_id": 1
+    }
+  ],
+  "upstreams": [
+    {
+      "id": 1,
+      "nodes": {
+        "127.0.0.1:1980": 1
+      },
+      "type": "roundrobin"
+    }
+  ]
+}
+--- request
+GET /hello
+--- response_body
+hello world
+
+
+
+=== TEST 2: not found upstream
+--- yaml_config eval: $::yaml_config
+--- apisix_json
+{
+  "routes": [
+    {
+      "id": 1,
+      "uri": "/hello",
+      "upstream_id": 1111
+    }
+  ],
+  "upstreams": [
+    {
+      "id": 1,
+      "nodes": {
+        "127.0.0.1:1980": 1
+      },
+      "type": "roundrobin"
+    }
+  ]
+}
+--- request
+GET /hello
+--- error_code_like: ^(?:50\d)$
+--- error_log
+failed to find upstream by id: 1111
+
+
+
+=== TEST 3: upstream_id priority upstream
+--- yaml_config eval: $::yaml_config
+--- apisix_json
+{
+  "routes": [
+    {
+      "id": 1,
+      "uri": "/hello",
+      "upstream_id": 1,
+      "upstream": {
+        "nodes": {
+          "127.0.0.1:1977": 1
+        },
+        "type": "roundrobin"
+      }
+    }
+  ],
+  "upstreams": [
+    {
+      "id": 1,
+      "nodes": {
+        "127.0.0.1:1981": 1
+      },
+      "type": "roundrobin"
+    }
+  ]
+}
+--- request
+GET /hello
+--- response_body
+hello world
+
+
+
+=== TEST 4: enable healthcheck
+--- yaml_config eval: $::yaml_config
+--- apisix_json
+{
+  "routes": [
+    {
+      "id": 1,
+      "uri": "/hello",
+      "upstream_id": 1
+    }
+  ],
+  "upstreams": [
+    {
+      "id": 1,
+      "nodes": {
+        "127.0.0.1:1980": 1
+      },
+      "type": "roundrobin",
+      "retries": 2,
+      "checks": {
+        "active": {
+          "http_path": "/status",
+          "healthy": {
+            "interval": 2,
+            "successes": 1
+          }
+        }
+      }
+    }
+  ]
+}
+--- request
+GET /hello
+--- response_body
+hello world
+
+
+
+=== TEST 5: upstream domain
+--- yaml_config eval: $::yaml_config
+--- apisix_json
+{
+  "routes": [
+    {
+      "id": 1,
+      "uri": "/hello",
+      "upstream_id": 1
+    }
+  ],
+  "upstreams": [
+    {
+      "id": 1,
+      "nodes": {
+        "test.com:1980": 1
+      },
+      "type": "roundrobin"
+    }
+  ]
+}
+--- request
+GET /hello
+--- error_code: 200
+
+
+
+=== TEST 6: upstream hash_on (bad)
+--- yaml_config eval: $::yaml_config
+--- apisix_json
+{
+  "routes": [
+    {
+      "id": 1,
+      "uri": "/hello",
+      "upstream_id": 1
+    }
+  ],
+  "upstreams": [
+    {
+      "id": 1,
+      "nodes": {
+        "test.com:1980": 1
+      },
+      "type": "chash",
+      "hash_on": "header",
+      "key": "$aaa"
+    }
+  ]
+}
+--- request
+GET /hello
+--- error_code: 502
+--- error_log
+invalid configuration: failed to match pattern
+
+
+
+=== TEST 7: upstream hash_on (good)
+--- yaml_config eval: $::yaml_config
+--- apisix_json
+{
+  "routes": [
+    {
+      "id": 1,
+      "uri": "/hello",
+      "upstream_id": 1
+    }
+  ],
+  "upstreams": [
+    {
+      "id": 1,
+      "nodes": {
+        "127.0.0.1:1980": 1,
+        "127.0.0.2:1980": 1
+      },
+      "type": "chash",
+      "hash_on": "header",
+      "key": "test"
+    }
+  ]
+}
+--- request
+GET /hello
+--- more_headers
+test: one
+--- error_log
+proxy request to 127.0.0.1:1980
diff --git a/t/config-center-json/route.t b/t/config-center-json/route.t
new file mode 100644
index 000000000..eef7b5e8e
--- /dev/null
+++ b/t/config-center-json/route.t
@@ -0,0 +1,364 @@
+#
+# 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';
+
+repeat_each(1);
+log_level('info');
+no_root_location();
+no_shuffle();
+
+run_tests();
+
+__DATA__
+
+=== TEST 1: sanity
+--- yaml_config eval: $::yaml_config
+--- apisix_json
+{
+  "routes": [
+    {
+      "id": 1,
+      "uri": "/hello",
+      "upstream": {
+        "nodes": {
+          "127.0.0.1:1980": 1
+        },
+        "type": "roundrobin"
+      }
+    }
+  ]
+}
+--- request
+GET /hello
+--- response_body
+hello world
+--- error_log
+use config_provider: yaml
+
+
+
+=== TEST 2: route:uri + host (missing host, not hit)
+--- yaml_config eval: $::yaml_config
+--- apisix_json
+{
+  "routes": [
+    {
+      "id": 1,
+      "uri": "/hello",
+      "host": "foo.com",
+      "upstream": {
+        "nodes": {
+          "127.0.0.1:1980": 1
+        },
+        "type": "roundrobin"
+      }
+    }
+  ]
+}
+--- request
+GET /hello
+--- error_code: 404
+--- error_log
+use config_provider: yaml
+
+
+
+=== TEST 3: route:uri + host
+--- yaml_config eval: $::yaml_config
+--- apisix_json
+{
+  "routes": [
+    {
+      "id": 1,
+      "uri": "/hello",
+      "host": "foo.com",
+      "upstream": {
+        "nodes": {
+          "127.0.0.1:1980": 1
+        },
+        "type": "roundrobin"
+      }
+    }
+  ]
+}
+--- more_headers
+host: foo.com
+--- request
+GET /hello
+--- response_body
+hello world
+
+
+
+=== TEST 4: route with bad plugin
+--- yaml_config eval: $::yaml_config
+--- apisix_json
+{
+  "routes": [
+    {
+      "id": 1,
+      "uri": "/hello",
+      "plugins": {
+        "proxy-rewrite": {
+          "uri": 1
+        }
+      },
+      "upstream": {
+        "nodes": {
+          "127.0.0.1:1980": 1
+        },
+        "type": "roundrobin"
+      }
+    }
+  ]
+}
+--- request
+GET /hello
+--- error_code: 404
+--- error_log
+property "uri" validation failed
+
+
+
+=== TEST 5: ignore unknown plugin
+--- yaml_config eval: $::yaml_config
+--- apisix_json
+{
+  "routes": [
+    {
+      "id": 1,
+      "uri": "/hello",
+      "plugins": {
+        "x-rewrite": {
+          "uri": 1
+        }
+      },
+      "upstream": {
+        "nodes": {
+          "127.0.0.1:1980": 1
+        },
+        "type": "roundrobin"
+      }
+    }
+  ]
+}
+--- request
+GET /hello
+--- response_body
+hello world
+
+
+
+=== TEST 6: route with bad plugin, radixtree_host_uri
+--- yaml_config
+apisix:
+    node_listen: 1984
+    router:
+        http: "radixtree_host_uri"
+deployment:
+    role: data_plane
+    role_data_plane:
+        config_provider: yaml
+--- apisix_json
+{
+  "routes": [
+    {
+      "id": 1,
+      "uri": "/hello",
+      "plugins": {
+        "proxy-rewrite": {
+          "uri": 1
+        }
+      },
+      "upstream": {
+        "nodes": {
+          "127.0.0.1:1980": 1
+        },
+        "type": "roundrobin"
+      }
+    }
+  ]
+}
+--- request
+GET /hello
+--- error_code: 404
+--- error_log
+property "uri" validation failed
+
+
+
+=== TEST 7: fix route with default value
+--- yaml_config
+apisix:
+    node_listen: 1984
+    router:
+        http: "radixtree_host_uri"
+deployment:
+    role: data_plane
+    role_data_plane:
+        config_provider: yaml
+--- apisix_json
+{
+  "routes": [
+    {
+      "id": 1,
+      "uri": "/hello",
+      "plugins": {
+        "uri-blocker": {
+          "block_rules": [
+            "/h*"
+          ]
+        }
+      },
+      "upstream": {
+        "nodes": {
+          "127.0.0.1:1980": 1
+        },
+        "type": "roundrobin"
+      }
+    }
+  ]
+}
+--- request
+GET /hello
+--- error_code: 403
+
+
+
+=== TEST 8: invalid route, bad vars operator
+--- yaml_config
+apisix:
+    node_listen: 1984
+    router:
+        http: "radixtree_host_uri"
+deployment:
+    role: data_plane
+    role_data_plane:
+        config_provider: yaml
+--- apisix_json
+{
+  "routes": [
+    {
+      "id": 1,
+      "uri": "/hello",
+      "vars": [
+        ["remote_addr", "=", "1"]
+      ],
+      "upstream": {
+        "nodes": {
+          "127.0.0.1:1980": 1
+        },
+        "type": "roundrobin"
+      }
+    }
+  ]
+}
+--- request
+GET /hello
+--- error_code: 404
+--- error_log
+failed to validate the 'vars' expression
+
+
+
+=== TEST 9: script with id
+--- yaml_config
+apisix:
+    node_listen: 1984
+deployment:
+    role: data_plane
+    role_data_plane:
+        config_provider: yaml
+--- apisix_json
+{
+  "routes": [
+    {
+      "id": 1,
+      "uri": "/hello",
+      "script": "local ngx = ngx",
+      "script_id": "1",
+      "upstream": {
+        "nodes": {
+          "127.0.0.1:1980": 1
+        },
+        "type": "roundrobin"
+      }
+    }
+  ]
+}
+--- request
+GET /hello
+--- error_code: 200
+--- error_log
+missing loaded script object
+
+
+
+=== TEST 10: hosts with '_' is valid
+--- yaml_config eval: $::yaml_config
+--- apisix_json
+{
+  "routes": [
+    {
+      "id": 1,
+      "uri": "/hello",
+      "hosts": [
+        "foo.com",
+        "v1_test-api.com"
+      ],
+      "upstream": {
+        "nodes": {
+          "127.0.0.1:1980": 1
+        },
+        "type": "roundrobin"
+      }
+    }
+  ]
+}
+--- more_headers
+host: v1_test-api.com
+--- request
+GET /hello
+--- response_body
+hello world
+
+
+
+=== TEST 11: script with plugin_config_id
+--- yaml_config eval: $::yaml_config
+--- apisix_json
+{
+  "routes": [
+    {
+      "id": 1,
+      "uri": "/hello",
+      "script": "local ngx = ngx",
+      "plugin_config_id": "1",
+      "upstream": {
+        "nodes": {
+          "127.0.0.1:1980": 1
+        },
+        "type": "roundrobin"
+      }
+    }
+  ]
+}
+--- request
+GET /hello
+--- error_code: 404
+--- error_log
+failed to check item data of [routes]
diff --git a/t/config-center-json/secret.t b/t/config-center-json/secret.t
new file mode 100644
index 000000000..178f19ef5
--- /dev/null
+++ b/t/config-center-json/secret.t
@@ -0,0 +1,458 @@
+#
+# 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';
+
+repeat_each(1);
+log_level('info');
+no_root_location();
+no_shuffle();
+
+add_block_preprocessor(sub {
+    my ($block) = @_;
+
+    if (!$block->apisix_json) {
+        my $json_config = <<_EOC_;
+{
+  "routes": [
+    {
+      "uri": "/hello",
+      "upstream": {
+        "nodes": {
+          "127.0.0.1:1980": 1
+        },
+        "type": "roundrobin"
+      }
+    }
+  ]
+}
+_EOC_
+
+        $block->set_value("apisix_json", $json_config);
+    }
+});
+
+run_tests();
+
+__DATA__
+
+=== TEST 1: validate secret/vault: wrong schema
+--- apisix_json
+{
+  "routes": [
+    {
+      "uri": "/hello",
+      "upstream": {
+        "nodes": {
+          "127.0.0.1:1980": 1
+        },
+        "type": "roundrobin"
+      }
+    }
+  ],
+  "secrets": [
+    {
+      "id": "vault/1",
+      "prefix": "kv/apisix",
+      "token": "root",
+      "uri": "127.0.0.1:8200"
+    }
+  ]
+}
+--- config
+    location /t {
+        content_by_lua_block {
+            local secret = require("apisix.secret")
+            local values = secret.secrets()
+            ngx.say(#values)
+        }
+    }
+--- request
+GET /t
+--- response_body
+0
+--- error_log
+property "uri" validation failed: failed to match pattern 
"^[^\\/]+:\\/\\/([\\da-zA-Z.-]+|\\[[\\da-fA-F:]+\\])(:\\d+)?"
+
+
+
+=== TEST 2: validate secrets: manager not exits
+--- apisix_json
+{
+  "routes": [
+    {
+      "uri": "/hello",
+      "upstream": {
+        "nodes": {
+          "127.0.0.1:1980": 1
+        },
+        "type": "roundrobin"
+      }
+    }
+  ],
+  "secrets": [
+    {
+      "id": "hhh/1",
+      "prefix": "kv/apisix",
+      "token": "root",
+      "uri": "127.0.0.1:8200"
+    }
+  ]
+}
+--- config
+    location /t {
+        content_by_lua_block {
+            local secret = require("apisix.secret")
+            local values = secret.secrets()
+            ngx.say(#values)
+        }
+    }
+--- request
+GET /t
+--- response_body
+0
+--- error_log
+secret manager not exits
+
+
+
+=== TEST 3: load config normal
+--- apisix_json
+{
+  "routes": [
+    {
+      "uri": "/hello",
+      "upstream": {
+        "nodes": {
+          "127.0.0.1:1980": 1
+        },
+        "type": "roundrobin"
+      }
+    }
+  ],
+  "secrets": [
+    {
+      "id": "vault/1",
+      "prefix": "kv/apisix",
+      "token": "root",
+      "uri": "http://127.0.0.1:8200";
+    }
+  ]
+}
+--- config
+    location /t {
+        content_by_lua_block {
+            local secret = require("apisix.secret")
+            local values = secret.secrets()
+            ngx.say("len: ", #values)
+
+            ngx.say("id: ", values[1].value.id)
+            ngx.say("prefix: ", values[1].value.prefix)
+            ngx.say("token: ", values[1].value.token)
+            ngx.say("uri: ", values[1].value.uri)
+        }
+    }
+--- request
+GET /t
+--- response_body
+len: 1
+id: vault/1
+prefix: kv/apisix
+token: root
+uri: http://127.0.0.1:8200
+
+
+
+=== TEST 4: store secret into vault
+--- exec
+VAULT_TOKEN='root' VAULT_ADDR='http://0.0.0.0:8200' vault kv put 
kv/apisix/apisix-key key=value
+--- response_body
+Success! Data written to: kv/apisix/apisix-key
+
+
+
+=== TEST 5: secret.fetch_by_uri: start with $secret://
+--- apisix_json
+{
+  "routes": [
+    {
+      "uri": "/hello",
+      "upstream": {
+        "nodes": {
+          "127.0.0.1:1980": 1
+        },
+        "type": "roundrobin"
+      }
+    }
+  ],
+  "secrets": [
+    {
+      "id": "vault/1",
+      "prefix": "kv/apisix",
+      "token": "root",
+      "uri": "http://127.0.0.1:8200";
+    }
+  ]
+}
+--- config
+    location /t {
+        content_by_lua_block {
+            local secret = require("apisix.secret")
+            local value = 
secret.fetch_by_uri("$secret://vault/1/apisix-key/key")
+            ngx.say(value)
+        }
+    }
+--- request
+GET /t
+--- response_body
+value
+
+
+
+=== TEST 6: secret.fetch_by_uri, wrong ref format: wrong type
+--- config
+    location /t {
+        content_by_lua_block {
+            local secret = require("apisix.secret")
+            local _, err = secret.fetch_by_uri(1)
+            ngx.say(err)
+        }
+    }
+--- request
+GET /t
+--- response_body
+error secret_uri type: number
+
+
+
+=== TEST 7: secret.fetch_by_uri, wrong ref format: wrong prefix
+--- config
+    location /t {
+        content_by_lua_block {
+            local secret = require("apisix.secret")
+            local _, err = secret.fetch_by_uri("secret://")
+            ngx.say(err)
+        }
+    }
+--- request
+GET /t
+--- response_body
+error secret_uri prefix: secret://
+
+
+
+=== TEST 8: secret.fetch_by_uri, error format: no secret manager
+--- config
+    location /t {
+        content_by_lua_block {
+            local secret = require("apisix.secret")
+            local _, err = secret.fetch_by_uri("$secret://")
+            ngx.say(err)
+        }
+    }
+--- request
+GET /t
+--- response_body
+error format: no secret manager
+
+
+
+=== TEST 9: secret.fetch_by_uri, error format: no secret conf id
+--- config
+    location /t {
+        content_by_lua_block {
+            local secret = require("apisix.secret")
+            local _, err = secret.fetch_by_uri("$secret://vault/")
+            ngx.say(err)
+        }
+    }
+--- request
+GET /t
+--- response_body
+error format: no secret conf id
+
+
+
+=== TEST 10: secret.fetch_by_uri, error format: no secret key id
+--- config
+    location /t {
+        content_by_lua_block {
+            local secret = require("apisix.secret")
+            local _, err = secret.fetch_by_uri("$secret://vault/2/")
+            ngx.say(err)
+        }
+    }
+--- request
+GET /t
+--- response_body
+error format: no secret key id
+
+
+
+=== TEST 11: secret.fetch_by_uri, no config
+--- config
+    location /t {
+        content_by_lua_block {
+            local secret = require("apisix.secret")
+            local _, err = secret.fetch_by_uri("$secret://vault/2/bar")
+            ngx.say(err)
+        }
+    }
+--- request
+GET /t
+--- response_body
+no secret conf, secret_uri: $secret://vault/2/bar
+
+
+
+=== TEST 12: secret.fetch_by_uri, no sub key value
+--- apisix_json
+{
+  "routes": [
+    {
+      "uri": "/hello",
+      "upstream": {
+        "nodes": {
+          "127.0.0.1:1980": 1
+        },
+        "type": "roundrobin"
+      }
+    }
+  ],
+  "secrets": [
+    {
+      "id": "vault/1",
+      "prefix": "kv/apisix",
+      "token": "root",
+      "uri": "http://127.0.0.1:8200";
+    }
+  ]
+}
+--- config
+    location /t {
+        content_by_lua_block {
+            local secret = require("apisix.secret")
+            local value = 
secret.fetch_by_uri("$secret://vault/1/apisix-key/bar")
+            ngx.say(value)
+        }
+    }
+--- request
+GET /t
+--- response_body
+nil
+
+
+
+=== TEST 13: fetch_secrets env: no cache
+--- main_config
+env secret=apisix;
+--- config
+    location /t {
+        content_by_lua_block {
+            local secret = require("apisix.secret")
+            local refs = {
+                key = "jack",
+                secret = "$env://secret"
+            }
+            local new_refs = secret.fetch_secrets(refs)
+            assert(new_refs ~= refs)
+            ngx.say(refs.secret)
+            ngx.say(new_refs.secret)
+            ngx.say(new_refs.key)
+        }
+    }
+--- request
+GET /t
+--- response_body
+$env://secret
+apisix
+jack
+--- error_log_like
+qr/retrieve secrets refs/
+
+
+
+=== TEST 14: fetch_secrets env: cache
+--- main_config
+env secret=apisix;
+--- config
+    location /t {
+        content_by_lua_block {
+            local secret = require("apisix.secret")
+            local refs = {
+                key = "jack",
+                secret = "$env://secret"
+            }
+            local refs_1 = secret.fetch_secrets(refs, true, "key", 1)
+            local refs_2 = secret.fetch_secrets(refs, true, "key", 1)
+            assert(refs_1 == refs_2)
+            ngx.say(refs_1.secret)
+            ngx.say(refs_2.secret)
+        }
+    }
+--- request
+GET /t
+--- response_body
+apisix
+apisix
+--- grep_error_log eval
+qr/retrieve secrets refs/
+--- grep_error_log_out
+retrieve secrets refs
+
+
+
+=== TEST 15: fetch_secrets env: table nesting
+--- main_config
+env secret=apisix;
+--- config
+    location /t {
+        content_by_lua_block {
+            local secret = require("apisix.secret")
+            local refs = {
+                key = "jack",
+                user = {
+                    username = "apisix",
+                    passsword = "$env://secret"
+                }
+            }
+            local new_refs = secret.fetch_secrets(refs)
+            ngx.say(new_refs.user.passsword)
+        }
+    }
+--- request
+GET /t
+--- response_body
+apisix
+
+
+
+=== TEST 16: fetch_secrets: wrong refs type
+--- main_config
+env secret=apisix;
+--- config
+    location /t {
+        content_by_lua_block {
+            local secret = require("apisix.secret")
+            local refs = "wrong"
+            local new_refs = secret.fetch_secrets(refs)
+            ngx.say(new_refs)
+        }
+    }
+--- request
+GET /t
+--- response_body
+nil
diff --git a/t/config-center-json/ssl.t b/t/config-center-json/ssl.t
new file mode 100644
index 000000000..f7637dec3
--- /dev/null
+++ b/t/config-center-json/ssl.t
@@ -0,0 +1,191 @@
+#
+# 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';
+
+repeat_each(1);
+log_level('debug');
+no_root_location();
+no_shuffle();
+
+add_block_preprocessor(sub {
+    my ($block) = @_;
+
+    if (!$block->request) {
+        $block->set_value("request", "GET /t");
+    }
+
+    if (!$block->no_error_log && !$block->error_log) {
+        $block->set_value("no_error_log", "[error]\n[alert]");
+    }
+
+    if ($block->sslhandshake) {
+        my $sslhandshake = $block->sslhandshake;
+
+        $block->set_value("config", <<_EOC_)
+listen unix:\$TEST_NGINX_HTML_DIR/nginx.sock ssl;
+
+location /t {
+    content_by_lua_block {
+        -- sync
+        ngx.sleep(0.2)
+
+        do
+            local sock = ngx.socket.tcp()
+
+            sock:settimeout(2000)
+
+            local ok, err = 
sock:connect("unix:\$TEST_NGINX_HTML_DIR/nginx.sock")
+            if not ok then
+                ngx.say("failed to connect: ", err)
+                return
+            end
+
+            $sslhandshake
+            local req = "GET /hello HTTP/1.0\\r\\nHost: 
test.com\\r\\nConnection: close\\r\\n\\r\\n"
+            local bytes, err = sock:send(req)
+            if not bytes then
+                ngx.say("failed to send http request: ", err)
+                return
+            end
+
+            local line, err = sock:receive()
+            if not line then
+                ngx.say("failed to receive: ", err)
+                return
+            end
+
+            ngx.say("received: ", line)
+
+            local ok, err = sock:close()
+            ngx.say("close: ", ok, " ", err)
+        end  -- do
+        -- collectgarbage()
+    }
+}
+_EOC_
+    }
+});
+
+run_tests();
+
+__DATA__
+
+=== TEST 1: sanity
+--- apisix_json
+{
+  "routes": [
+    {
+      "uri": "/hello",
+      "upstream": {
+        "nodes": {
+          "127.0.0.1:1980": 1
+        },
+        "type": "roundrobin"
+      }
+    }
+  ],
+  "ssls": [
+    {
+      "cert": "-----BEGIN 
CERTIFICATE-----\nMIIDrzCCApegAwIBAgIJAI3Meu/gJVTLMA0GCSqGSIb3DQEBCwUAMG4xCzAJBgNV\nBAYTAkNOMREwDwYDVQQIDAhaaGVqaWFuZzERMA8GA1UEBwwISGFuZ3pob3UxDTAL\nBgNVBAoMBHRlc3QxDTALBgNVBAsMBHRlc3QxGzAZBgNVBAMMEmV0Y2QuY2x1c3Rl\nci5sb2NhbDAeFw0yMDEwMjgwMzMzMDJaFw0yMTEwMjgwMzMzMDJaMG4xCzAJBgNV\nBAYTAkNOMREwDwYDVQQIDAhaaGVqaWFuZzERMA8GA1UEBwwISGFuZ3pob3UxDTAL\nBgNVBAoMBHRlc3QxDTALBgNVBAsMBHRlc3QxGzAZBgNVBAMMEmV0Y2QuY2x1c3Rl\nci5sb2NhbDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
 [...]
+      "key": "-----BEGIN PRIVATE 
KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCf6sMQke4OUrPf\nlZXqKZC4uaQLMxJB2DgacBf4Q3kXcVNCNHtc2U3U5cMD8tr3/Jt5MKKun5jQrbXV\nFF4eVr4Iv9jgPBwQc2kBUC9QL/alsMhEuXMeqdGQcCK3F0CLJdV3zUlKUDU0kg+O\nExnbl1CHXrIbpD7zLy1i3s8p39v1pYFYf4WlrQxvfa/xo97gXY5dJv8RryryLzRc\nuhHYBvX5MHCGpbrY61JxpfZqBo8CmLuHl1tmbeXpdHdQB11LKiuL6HtKflNjc6rg\n5r8bXl1nZbM/KOZEE+muA1LVoaTyHzY/aGXz0bNy4QRUO+De9JFcTDgnXnNZVG5x\ncyyDBpc9AgMBAAECggEAatcEtehZPJaCeClPPF/Cwbe9YoIfe4BCk
 [...]
+      "snis": [
+        "t.com",
+        "test.com"
+      ]
+    }
+  ]
+}
+--- sslhandshake
+local sess, err = sock:sslhandshake(nil, "test.com", false)
+if not sess then
+    ngx.say("failed to do SSL handshake: ", err)
+    return
+end
+--- response_body
+received: HTTP/1.1 200 OK
+close: 1 nil
+--- error_log
+server name: "test.com"
+
+
+
+=== TEST 2: single sni
+--- apisix_json
+{
+  "routes": [
+    {
+      "uri": "/hello",
+      "upstream": {
+        "nodes": {
+          "127.0.0.1:1980": 1
+        },
+        "type": "roundrobin"
+      }
+    }
+  ],
+  "ssls": [
+    {
+      "cert": "-----BEGIN 
CERTIFICATE-----\nMIIDrzCCApegAwIBAgIJAI3Meu/gJVTLMA0GCSqGSIb3DQEBCwUAMG4xCzAJBgNV\nBAYTAkNOMREwDwYDVQQIDAhaaGVqaWFuZzERMA8GA1UEBwwISGFuZ3pob3UxDTAL\nBgNVBAoMBHRlc3QxDTALBgNVBAsMBHRlc3QxGzAZBgNVBAMMEmV0Y2QuY2x1c3Rl\nci5sb2NhbDAeFw0yMDEwMjgwMzMzMDJaFw0yMTEwMjgwMzMzMDJaMG4xCzAJBgNV\nBAYTAkNOMREwDwYDVQQIDAhaaGVqaWFuZzERMA8GA1UEBwwISGFuZ3pob3UxDTAL\nBgNVBAoMBHRlc3QxDTALBgNVBAsMBHRlc3QxGzAZBgNVBAMMEmV0Y2QuY2x1c3Rl\nci5sb2NhbDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
 [...]
+      "key": "-----BEGIN PRIVATE 
KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCf6sMQke4OUrPf\nlZXqKZC4uaQLMxJB2DgacBf4Q3kXcVNCNHtc2U3U5cMD8tr3/Jt5MKKun5jQrbXV\nFF4eVr4Iv9jgPBwQc2kBUC9QL/alsMhEuXMeqdGQcCK3F0CLJdV3zUlKUDU0kg+O\nExnbl1CHXrIbpD7zLy1i3s8p39v1pYFYf4WlrQxvfa/xo97gXY5dJv8RryryLzRc\nuhHYBvX5MHCGpbrY61JxpfZqBo8CmLuHl1tmbeXpdHdQB11LKiuL6HtKflNjc6rg\n5r8bXl1nZbM/KOZEE+muA1LVoaTyHzY/aGXz0bNy4QRUO+De9JFcTDgnXnNZVG5x\ncyyDBpc9AgMBAAECggEAatcEtehZPJaCeClPPF/Cwbe9YoIfe4BCk
 [...]
+      "sni": "test.com"
+    }
+  ]
+}
+--- sslhandshake
+local sess, err = sock:sslhandshake(nil, "test.com", false)
+if not sess then
+    ngx.say("failed to do SSL handshake: ", err)
+    return
+end
+--- response_body
+received: HTTP/1.1 200 OK
+close: 1 nil
+--- error_log
+server name: "test.com"
+
+
+
+=== TEST 3: bad cert
+--- apisix_json
+{
+  "routes": [
+    {
+      "uri": "/hello",
+      "upstream": {
+        "type": "roundrobin",
+        "nodes": {
+          "httpbin.org:80": 1
+        }
+      },
+      "ssl_enable": true
+    }
+  ],
+  "ssls": [
+    {
+      "cert": "-----BEGIN 
CERTIFICATE-----\nMIIDrzCCApegAwIBAgIJAI3Meu/gJVTLMA0GCSqGSIb3DQEBCwUAMG4xCzAJBgNV\nBAYTAkNOMREwDwYDVQQIDAhaaGVqaWFuZzERMA8GA1UEBwwISGFuZ3pob3UxDTAL\nBgNVBAoMBHRlc3QxDTALBgNVBAsMBHRlc3QxGzAZBgNVBAMMEmV0Y2QuY2x1c3Rl\nci5sb2NhbDAeFw0yMDEwMjgwMzMzMDJaFw0yMTEwMjgwMzMzMDJaMG4xCzAJBgNV\nBAYTAkNOMREwDwYDVQQIDAhaaGVqaWFuZzERMA8GA1UEBwwISGFuZ3pob3UxDTAL\nBgNVBAoMBHRlc3QxDTALBgNVBAsMBHRlc3QxGzAZBgNVBAMMEmV0Y2QuY2x1c3Rl\nci5sb2NhbDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
 [...]
+      "key": "-----BEGIN PRIVATE 
KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCf6sMQke4OUrPf\nlZXqKZC4uaQLMxJB2DgacBf4Q3kXcVNCNHtc2U3U5cMD8tr3/Jt5MKKun5jQrbXV\nFF4eVr4Iv9jgPBwQc2kBUC9QL/alsMhEuXMeqdGQcCK3F0CLJdV3zUlKUDU0kg+O\nExnbl1CHXrIbpD7zLy1i3s8p39v1pYFYf4WlrQxvfa/xo97gXY5dJv8RryryLzRc\nuhHYBvX5MHCGpbrY61JxpfZqBo8CmLuHl1tmbeXpdHdQB11LKiuL6HtKflNjc6rg\n5r8bXl1nZbM/KOZEE+muA1LVoaTyHzY/aGXz0bNy4QRUO+De9JFcTDgnXnNZVG5x\ncyyDBpc9AgMBAAECggEAatcEtehZPJaCeClPPF/Cwbe9YoIfe4BCk
 [...]
+      "snis": [
+        "t.com",
+        "test.com"
+      ]
+    }
+  ]
+}
+
+--- error_log
+failed to parse cert
+--- error_code: 404
diff --git a/t/config-center-json/stream-route.t 
b/t/config-center-json/stream-route.t
new file mode 100644
index 000000000..47eb16eff
--- /dev/null
+++ b/t/config-center-json/stream-route.t
@@ -0,0 +1,148 @@
+#
+# 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';
+
+repeat_each(1);
+log_level('info');
+no_root_location();
+no_shuffle();
+
+add_block_preprocessor(sub {
+    my ($block) = @_;
+
+    $block->set_value("stream_enable", 1);
+
+    if (!$block->stream_request) {
+        $block->set_value("stream_request", "mmm");
+    }
+
+    if (!$block->error_log && !$block->no_error_log) {
+        $block->set_value("no_error_log", "[error]\n[alert]");
+    }
+});
+
+run_tests();
+
+__DATA__
+
+=== TEST 1: sanity
+--- apisix_json
+{
+  "stream_routes": [
+    {
+      "server_addr": "127.0.0.1",
+      "server_port": 1985,
+      "id": 1,
+      "upstream": {
+        "nodes": {
+          "127.0.0.1:1995": 1
+        },
+        "type": "roundrobin"
+      }
+    }
+  ]
+}
+--- stream_response
+hello world
+
+
+
+=== TEST 2: rule with bad plugin
+--- apisix_json
+{
+  "stream_routes": [
+    {
+      "server_addr": "127.0.0.1",
+      "server_port": 1985,
+      "id": 1,
+      "plugins": {
+        "mqtt-proxy": {
+          "uri": 1
+        }
+      },
+      "upstream": {
+        "nodes": {
+          "127.0.0.1:1995": 1
+        },
+        "type": "roundrobin"
+      }
+    }
+  ]
+}
+--- error_log eval
+qr/property "\w+" is required/
+
+
+
+=== TEST 3: ignore unknown plugin
+--- apisix_json
+{
+  "stream_routes": [
+    {
+      "server_addr": "127.0.0.1",
+      "server_port": 1985,
+      "id": 1,
+      "plugins": {
+        "x-rewrite": {
+          "uri": 1
+        }
+      },
+      "upstream": {
+        "nodes": {
+          "127.0.0.1:1995": 1
+        },
+        "type": "roundrobin"
+      }
+    }
+  ]
+}
+--- stream_response
+hello world
+
+
+
+=== TEST 4: sanity with plugin
+--- apisix_json
+{
+  "stream_routes": [
+    {
+      "server_addr": "127.0.0.1",
+      "server_port": 1985,
+      "id": 1,
+      "upstream_id": 1,
+      "plugins": {
+        "mqtt-proxy": {
+          "protocol_name": "MQTT",
+          "protocol_level": 4
+        }
+      }
+    }
+  ],
+  "upstreams": [
+    {
+      "nodes": {
+        "127.0.0.1:1995": 1
+      },
+      "type": "roundrobin",
+      "id": 1
+    }
+  ]
+}
+--- stream_request eval
+"\x10\x0f\x00\x04\x4d\x51\x54\x54\x04\x02\x00\x3c\x00\x03\x66\x6f\x6f"
+--- stream_response
+hello world

Reply via email to