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