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

baoyuan 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 41775ef5a fix: resolve env vars before YAML parsing to preserve types 
in standalone mode (#13078)
41775ef5a is described below

commit 41775ef5a15bb2f7559e7a185c3e0f1d0d111b64
Author: Yuhan <[email protected]>
AuthorDate: Thu Apr 16 14:57:16 2026 +0800

    fix: resolve env vars before YAML parsing to preserve types in standalone 
mode (#13078)
---
 apisix/cli/file.lua         |  26 ++-
 apisix/core/config_yaml.lua |  20 +-
 t/cli/test_standalone.sh    | 507 ++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 540 insertions(+), 13 deletions(-)

diff --git a/apisix/cli/file.lua b/apisix/cli/file.lua
index 36873631a..bbdb738e3 100644
--- a/apisix/cli/file.lua
+++ b/apisix/cli/file.lua
@@ -90,6 +90,17 @@ local function var_sub(val)
 end
 
 
+-- Substitute env vars in raw text before parsing. YAML parser then infers
+-- types naturally: `"${{VAR}}"` stays string, `${{VAR}}` infers from value.
+local function resolve_conf_var_in_text(text)
+    local new_text, _, err = var_sub(text)
+    if err then
+        return nil, err
+    end
+    return new_text
+end
+
+
 local function resolve_conf_var(conf)
     local new_keys = {}
     for key, val in pairs(conf) do
@@ -143,6 +154,7 @@ end
 
 
 _M.resolve_conf_var = resolve_conf_var
+_M.resolve_conf_var_in_text = resolve_conf_var_in_text
 
 
 local function replace_by_reserved_env_vars(conf)
@@ -302,12 +314,14 @@ function _M.read_yaml_conf(apisix_home)
         local apisix_conf_path = profile:yaml_path("apisix")
         local apisix_conf_yaml, _ = util.read_file(apisix_conf_path)
         if apisix_conf_yaml then
-            local apisix_conf = yaml.load(apisix_conf_yaml)
-            if apisix_conf then
-                local ok, err = resolve_conf_var(apisix_conf)
-                if not ok then
-                    return nil, err
-                end
+            -- Pre-parse substitution: `"${{VAR}}"` stays string, `${{VAR}}` 
infers type from value.
+            local resolved, err = resolve_conf_var_in_text(apisix_conf_yaml)
+            if not resolved then
+                return nil, err
+            end
+            local apisix_conf = yaml.load(resolved)
+            if not apisix_conf then
+                return nil, "invalid apisix.yaml file"
             end
         end
     end
diff --git a/apisix/core/config_yaml.lua b/apisix/core/config_yaml.lua
index d0443f256..a57d15dd6 100644
--- a/apisix/core/config_yaml.lua
+++ b/apisix/core/config_yaml.lua
@@ -96,7 +96,13 @@ local config_yaml = {
         local raw_config = f:read("*a")
         f:close()
 
-        return yaml.load(raw_config), nil
+        -- substitute env vars in the raw YAML text before parsing so that
+        -- YAML quoting decides types (see apisix/cli/file.lua).
+        local resolved, err = file.resolve_conf_var_in_text(raw_config)
+        if not resolved then
+            return nil, err
+        end
+        return yaml.load(resolved), nil
     end
 }
 
@@ -116,6 +122,12 @@ local config_json = {
         if err then
             return nil, "failed to decode json: " .. err
         end
+        -- JSON has explicit types, so env var substitution is applied
+        -- post-parse (same behavior as before).
+        local ok, err = file.resolve_conf_var(config)
+        if not ok then
+            return nil, err
+        end
         return config, nil
     end
 }
@@ -153,12 +165,6 @@ local function update_config(table, conf_version)
         return
     end
 
-    local ok, err = file.resolve_conf_var(table)
-    if not ok then
-        log.error("failed to resolve variables:" .. err)
-        return
-    end
-
     apisix_yaml = table
     sync_status_to_shdict(true)
     apisix_yaml_mtime = conf_version
diff --git a/t/cli/test_standalone.sh b/t/cli/test_standalone.sh
index d5844a2ce..c1e728d7a 100755
--- a/t/cli/test_standalone.sh
+++ b/t/cli/test_standalone.sh
@@ -155,3 +155,510 @@ if [ $expected_config_reloads -ne $actual_config_reloads 
]; then
     exit 1
 fi
 echo "passed: apisix.yaml was not reloaded"
+
+make stop
+sleep 0.5
+
+# test: environment variable with large number should be preserved as string
+echo '
+apisix:
+  enable_admin: false
+deployment:
+  role: data_plane
+  role_data_plane:
+    config_provider: yaml
+' > conf/config.yaml
+
+echo '
+routes:
+  -
+    uri: /test-large-number
+    plugins:
+      response-rewrite:
+        body: "${{APISIX_CLIENT_ID}}"
+        status_code: 200
+    upstream:
+      nodes:
+        "127.0.0.1:9091": 1
+      type: roundrobin
+#END
+' > conf/apisix.yaml
+
+# Test with large number that exceeds Lua double precision
+APISIX_CLIENT_ID="356002209726529540" make init
+
+if ! APISIX_CLIENT_ID="356002209726529540" make run > output.log 2>&1; then
+    cat output.log
+    echo "failed: large number in env var should not cause type conversion 
error"
+    exit 1
+fi
+
+sleep 0.1
+
+# Verify the response body matches the exact large numeric string
+code=$(curl -o /tmp/response_body -s -m 5 -w %{http_code} 
http://127.0.0.1:9080/test-large-number)
+body=$(cat /tmp/response_body)
+if [ "$code" -ne 200 ]; then
+    echo "failed: expected 200 for /test-large-number, but got: $code, body: 
$body"
+    exit 1
+fi
+if [ "$body" != "356002209726529540" ]; then
+    echo "failed: large number env var was not preserved as string, got: $body"
+    exit 1
+fi
+
+make stop
+sleep 0.5
+
+echo "passed: large number in env var preserved as string in apisix.yaml"
+
+# test: small numeric env vars in apisix.yaml should still be converted to 
number
+echo '
+apisix:
+  enable_admin: false
+deployment:
+  role: data_plane
+  role_data_plane:
+    config_provider: yaml
+' > conf/config.yaml
+
+echo '
+routes:
+  -
+    uri: /test-small-number
+    plugins:
+      response-rewrite:
+        body: "hello"
+        status_code: ${{REWRITE_STATUS}}
+    upstream:
+      nodes:
+        "127.0.0.1:9091": 1
+      type: roundrobin
+#END
+' > conf/apisix.yaml
+
+REWRITE_STATUS="200" make init
+
+if ! REWRITE_STATUS="200" make run > output.log 2>&1; then
+    cat output.log
+    echo "failed: small numeric env var should be converted to number in 
apisix.yaml"
+    exit 1
+fi
+
+sleep 0.1
+
+code=$(curl -o /tmp/response_body -s -m 5 -w %{http_code} 
http://127.0.0.1:9080/test-small-number)
+body=$(cat /tmp/response_body)
+if [ "$code" -ne 200 ]; then
+    echo "failed: expected 200 for /test-small-number, but got: $code, body: 
$body"
+    exit 1
+fi
+
+make stop
+sleep 0.5
+
+echo "passed: small numeric env var converted to number in apisix.yaml"
+
+# test: config.yaml should still support type conversion (boolean)
+echo '
+routes: []
+#END
+' > conf/apisix.yaml
+
+echo '
+apisix:
+  enable_admin: ${{ENABLE_ADMIN}}
+deployment:
+  role: traditional
+  role_traditional:
+    config_provider: yaml
+  etcd:
+    host:
+      - "http://127.0.0.1:2379";
+' > conf/config.yaml
+
+ENABLE_ADMIN=false make init
+ENABLE_ADMIN=false make run
+sleep 0.1
+
+# If type conversion works, enable_admin is boolean false and admin API is 
disabled (404)
+# If type conversion fails, enable_admin stays string "false" which is truthy, 
admin API is enabled
+code=$(curl -o /dev/null -s -m 5 -w %{http_code} 
http://127.0.0.1:9080/apisix/admin/routes)
+if [ "$code" -ne 404 ]; then
+    echo "failed: expected 404 when admin API is disabled, but got: $code"
+    exit 1
+fi
+
+make stop
+sleep 0.5
+
+echo "passed: config.yaml still converts boolean env vars correctly"
+
+# test: numeric env vars for upstream weight and retries in apisix.yaml
+echo '
+apisix:
+  enable_admin: false
+deployment:
+  role: data_plane
+  role_data_plane:
+    config_provider: yaml
+' > conf/config.yaml
+
+echo '
+routes:
+  -
+    uri: /test-upstream-env
+    plugins:
+      proxy-rewrite:
+        uri: /apisix/nginx_status
+    upstream:
+      nodes:
+        "127.0.0.1:9091": ${{WEIGHT}}
+      type: roundrobin
+      retries: ${{RETRIES}}
+#END
+' > conf/apisix.yaml
+
+WEIGHT="1" RETRIES="3" make init
+
+if ! WEIGHT="1" RETRIES="3" make run > output.log 2>&1; then
+    cat output.log
+    echo "failed: numeric env vars for weight/retries should be converted to 
number"
+    exit 1
+fi
+
+sleep 0.1
+
+code=$(curl -o /dev/null -s -m 5 -w %{http_code} 
http://127.0.0.1:9080/test-upstream-env)
+if [ "$code" -ne 200 ]; then
+    echo "failed: expected 200 for /test-upstream-env, but got: $code"
+    exit 1
+fi
+
+make stop
+sleep 0.5
+
+echo "passed: numeric env vars for upstream weight and retries converted to 
number in apisix.yaml"
+
+# test: boolean env vars in apisix.yaml should be converted to boolean
+echo '
+apisix:
+  enable_admin: false
+deployment:
+  role: data_plane
+  role_data_plane:
+    config_provider: yaml
+' > conf/config.yaml
+
+echo '
+routes:
+  -
+    uri: /test-bool-env
+    plugins:
+      redirect:
+        http_to_https: ${{REDIRECT_HTTPS}}
+    upstream:
+      nodes:
+        "127.0.0.1:9091": 1
+      type: roundrobin
+#END
+' > conf/apisix.yaml
+
+REDIRECT_HTTPS="true" make init
+
+if ! REDIRECT_HTTPS="true" make run > output.log 2>&1; then
+    cat output.log
+    echo "failed: boolean env var should be converted to boolean in 
apisix.yaml"
+    exit 1
+fi
+
+sleep 0.1
+
+# If boolean conversion works, redirect plugin returns 301
+code=$(curl -o /dev/null -s -m 5 -w %{http_code} 
http://127.0.0.1:9080/test-bool-env)
+if [ "$code" -ne 301 ]; then
+    echo "failed: expected 301 redirect for /test-bool-env, but got: $code"
+    exit 1
+fi
+
+make stop
+sleep 0.5
+
+echo "passed: boolean env var converted to boolean in apisix.yaml"
+
+# test: config.yaml should still support numeric type conversion
+echo '
+routes: []
+#END
+' > conf/apisix.yaml
+
+echo '
+apisix:
+  resolver_timeout: ${{RESOLVER_TIMEOUT}}
+deployment:
+  role: data_plane
+  role_data_plane:
+    config_provider: yaml
+' > conf/config.yaml
+
+if ! RESOLVER_TIMEOUT=5 make init > output.log 2>&1; then
+    cat output.log
+    echo "failed: config.yaml should convert numeric env vars to number"
+    exit 1
+fi
+
+echo "passed: config.yaml still converts numeric env vars correctly"
+
+# test: small numeric env var inside quoted string should stay as string
+# (the exact scenario from issue #12932 — key-auth key expects a string,
+#  previously substituted numeric values were coerced to numbers and failed
+#  schema validation)
+echo '
+apisix:
+  enable_admin: false
+deployment:
+  role: data_plane
+  role_data_plane:
+    config_provider: yaml
+' > conf/config.yaml
+
+echo '
+routes:
+  -
+    uri: /test-quoted-numeric
+    plugins:
+      key-auth: {}
+      proxy-rewrite:
+        uri: /apisix/nginx_status
+    upstream:
+      nodes:
+        "127.0.0.1:9091": 1
+      type: roundrobin
+consumers:
+  -
+    username: testuser
+    plugins:
+      key-auth:
+        key: "${{TEST_KEY}}"
+#END
+' > conf/apisix.yaml
+
+TEST_KEY="12345" make init
+
+if ! TEST_KEY="12345" make run > output.log 2>&1; then
+    cat output.log
+    echo "failed: quoted numeric env var should stay string and pass schema 
validation"
+    exit 1
+fi
+
+sleep 0.1
+
+# With correct key header → 200
+code=$(curl -o /dev/null -s -m 5 -w %{http_code} -H "apikey: 12345" 
http://127.0.0.1:9080/test-quoted-numeric)
+if [ "$code" -ne 200 ]; then
+    echo "failed: expected 200 with correct apikey, but got: $code"
+    cat logs/error.log
+    exit 1
+fi
+
+# Without header → 401
+code=$(curl -o /dev/null -s -m 5 -w %{http_code} 
http://127.0.0.1:9080/test-quoted-numeric)
+if [ "$code" -ne 401 ]; then
+    echo "failed: expected 401 without apikey, but got: $code"
+    exit 1
+fi
+
+make stop
+sleep 0.5
+
+echo "passed: quoted numeric env var preserved as string for key-auth consumer 
key"
+
+# test: boolean env var inside quoted string should stay as string
+# (previously a quoted "${{V}}" with V=true got post-parse coerced to a Lua
+#  boolean, which failed schema validation for string-typed plugin fields)
+echo '
+apisix:
+  enable_admin: false
+deployment:
+  role: data_plane
+  role_data_plane:
+    config_provider: yaml
+' > conf/config.yaml
+
+echo '
+routes:
+  -
+    uri: /test-quoted-bool
+    plugins:
+      response-rewrite:
+        body: "${{BODY_VAL}}"
+        status_code: 200
+    upstream:
+      nodes:
+        "127.0.0.1:9091": 1
+      type: roundrobin
+#END
+' > conf/apisix.yaml
+
+BODY_VAL="true" make init
+
+if ! BODY_VAL="true" make run > output.log 2>&1; then
+    cat output.log
+    echo "failed: quoted boolean env var should stay string"
+    exit 1
+fi
+
+sleep 0.1
+
+code=$(curl -o /tmp/response_body -s -m 5 -w %{http_code} 
http://127.0.0.1:9080/test-quoted-bool)
+body=$(cat /tmp/response_body)
+if [ "$code" -ne 200 ]; then
+    echo "failed: expected 200 for /test-quoted-bool, but got: $code, body: 
$body"
+    exit 1
+fi
+if [ "$body" != "true" ]; then
+    echo "failed: quoted bool env var was not preserved as string, got: $body"
+    exit 1
+fi
+
+make stop
+sleep 0.5
+
+echo "passed: quoted boolean env var preserved as string in apisix.yaml"
+
+# test: default value fallback still works for unset env var
+echo '
+apisix:
+  enable_admin: false
+deployment:
+  role: data_plane
+  role_data_plane:
+    config_provider: yaml
+' > conf/config.yaml
+
+echo '
+routes:
+  -
+    uri: /test-default-val
+    plugins:
+      response-rewrite:
+        body: "hello"
+        status_code: ${{UNSET_STATUS:=202}}
+    upstream:
+      nodes:
+        "127.0.0.1:9091": 1
+      type: roundrobin
+#END
+' > conf/apisix.yaml
+
+unset UNSET_STATUS
+make init
+
+if ! make run > output.log 2>&1; then
+    cat output.log
+    echo "failed: default value fallback should work"
+    exit 1
+fi
+
+sleep 0.1
+
+code=$(curl -o /dev/null -s -m 5 -w %{http_code} 
http://127.0.0.1:9080/test-default-val)
+if [ "$code" -ne 202 ]; then
+    echo "failed: expected 202 from default fallback, but got: $code"
+    exit 1
+fi
+
+make stop
+sleep 0.5
+
+echo "passed: default value fallback (\${{VAR:=default}}) works"
+
+# test: env var substitution inside a YAML key
+echo '
+apisix:
+  enable_admin: false
+deployment:
+  role: data_plane
+  role_data_plane:
+    config_provider: yaml
+' > conf/config.yaml
+
+echo '
+routes:
+  -
+    uri: /test-key-sub
+    plugins:
+      "${{PLUGIN_NAME}}":
+        body: "key-sub-ok"
+        status_code: 200
+    upstream:
+      nodes:
+        "127.0.0.1:9091": 1
+      type: roundrobin
+#END
+' > conf/apisix.yaml
+
+PLUGIN_NAME="response-rewrite" make init
+
+if ! PLUGIN_NAME="response-rewrite" make run > output.log 2>&1; then
+    cat output.log
+    echo "failed: env var in YAML key should be substituted before parsing"
+    exit 1
+fi
+
+sleep 0.1
+
+code=$(curl -o /tmp/response_body -s -m 5 -w %{http_code} 
http://127.0.0.1:9080/test-key-sub)
+body=$(cat /tmp/response_body)
+if [ "$code" -ne 200 ] || [ "$body" != "key-sub-ok" ]; then
+    echo "failed: expected 200/key-sub-ok for /test-key-sub, got code: $code 
body: $body"
+    exit 1
+fi
+
+make stop
+sleep 0.5
+
+echo "passed: env var substitution inside YAML key"
+
+# test: missing env var (no default) should produce a clear startup error
+echo '
+apisix:
+  enable_admin: false
+deployment:
+  role: data_plane
+  role_data_plane:
+    config_provider: yaml
+' > conf/config.yaml
+
+echo '
+routes:
+  -
+    uri: /test-missing-var
+    plugins:
+      response-rewrite:
+        body: "hello"
+        status_code: ${{DEFINITELY_NOT_SET_VAR}}
+    upstream:
+      nodes:
+        "127.0.0.1:9091": 1
+      type: roundrobin
+#END
+' > conf/apisix.yaml
+
+unset DEFINITELY_NOT_SET_VAR
+if make init > output.log 2>&1; then
+    echo "failed: make init should fail when required env var is missing"
+    cat output.log
+    exit 1
+fi
+
+if ! grep "can't find environment variable DEFINITELY_NOT_SET_VAR" output.log 
> /dev/null; then
+    echo "failed: expected missing-env-var error message in init output"
+    cat output.log
+    exit 1
+fi
+
+echo "passed: missing env var produces clear startup error"
+
+git checkout conf/config.yaml
+git checkout conf/apisix.yaml

Reply via email to