Problem – HAProxy reload fails with “invalid function call format”
During automated deployments in a GitLab CI pipeline the HAProxy service aborts its reload step. The CI job reports a validation error such as:
ERROR: configuration file contains an invalid function call format at line 42
parse error: unexpected token ‘(‘ in fetch expression
Invalid fetch/convert syntax: %[var(…] – missing closing bracket or parenthesis
The failure is reproducible only when the configuration is generated from templates that interpolate environment variables and service‑discovery data. Manual edits of the same file (or a hand‑crafted static config) reload without issue.
Root Cause – Mis‑formatted fetch/converter expressions
HAProxy’s expression language requires a strict %[ ... ] syntax. Inside the brackets a fetch (e.g. var(req.user)) may be followed by zero or more converters (e.g. str()). The official Configuration Syntax documentation states:
- Fetches are written as
fetch_name(arg1,arg2)– parentheses are mandatory. - Converters are appended with their own parentheses, e.g.
str(%[var(req.user)]). - The entire expression must be enclosed in
%[ … ]with matching brackets.
When a templating engine (Jinja2, Go‑text/template, etc.) expands variables, missing or mismatched parentheses/brackets are introduced. Typical patterns observed in real incidents include:
| Incorrect snippet | Reason |
|---|---|
|
Correct – both parentheses present. |
|
Missing closing parenthesis after var(req.user). |
|
Correct when API_PATH is defined. |
|
Environment substitution produced an empty string, leaving an empty fetch. |
|
Correct. |
|
Missing closing bracket. |
HAProxy’s runtime API (set-var, use_backend) validates the same syntax at reload time; any deviation triggers the “invalid function call format” error.
Debug – Investigating the malformed configuration
- Run HAProxy validation locally. In the CI job add a step:
haproxy -c -f generated/haproxy.cfg
Typical output:
[ERR] line 42: invalid function call format – missing closing parenthesis in fetch expression
[INFO] configuration file contains 1 error(s)
sed -n '40,44p' generated/haproxy.cfg to view surrounding context.# line 40
http-request set-var(req.user) str(%[var(req.user]) # ← malformed
# line 41
acl is_api path_beg %[] # ← empty fetch
# line 42
use_backend %[var(req.backend]
jinja2 --debug-template templates/haproxy.cfg.j2 -D API_PATH=$API_PATH -o generated/haproxy.cfg
Look for warnings about “undefined variable” or “missing closing tag”.
echo "API_PATH=$API_PATH"
If API_PATH is empty, the resulting ACL becomes %[], which HAProxy cannot parse.
systemctl reload haproxy, the service manager logs the error:systemd[1]: haproxy.service: Reloading.
systemd[1]: haproxy.service: Failed to reload configuration: invalid function call format at line 42
Solution – Ensure well‑formed fetch/converter syntax in generated configs
1. Guard template expressions with defaults
For Jinja2, use the default filter to guarantee a non‑empty string:
{% set api_path = env.API_PATH | default('/api') %}
acl is_api path_beg %[var(env.API_PATH)]
Resulting snippet (after rendering):
acl is_api path_beg %[var(env.API_PATH)]
2. Enforce matching parentheses/brackets with a linter
Add a CI step that runs a simple regex‑based check:
#!/usr/bin/env bash
cfg=generated/haproxy.cfg
if grep -P '(%\[[^\]]*$|%[^\[]*$|[^)]\s*%|\[[^\]]*$' "$cfg" >/dev/null; then
echo "⚠️ Detected possible mismatched %[ ... ] expression"
exit 1
fi
3. Refactor complex expressions into variables
Instead of embedding long fetch chains directly, store them in a set-var earlier in the frontend and reference the variable later:
# Before (error‑prone)
http-request set-var(req.user) str(%[var(req.user)])
# After (clear separation)
http-request set-var(req.user) %[var(req.user)]
http-request set-var(req.user.str) str(%[var(req.user)])
4. Update the CI template to quote dynamic values
When inserting raw strings into HAProxy directives, wrap them in double quotes to prevent the templating engine from stripping required characters:
use_backend "{{ backend_name | default('default_backend') }}"
5. Example – Fixed configuration
Below is a before/after comparison of a problematic snippet and its corrected version.
| Before (fails) | After (passes) |
|---|---|
|
|
Verify – Confirm that HAProxy reloads cleanly
- Run the validation command again:
haproxy -c -f generated/haproxy.cfg
Expected output:
[INFO] Configuration file is valid
[INFO] 0 errors found
- Trigger a reload in the pipeline (or locally):
systemctl reload haproxy
Check the service status:
systemctl status haproxy
Expected snippet:
haproxy.service – HAProxy Load Balancer
Loaded: loaded (/etc/systemd/system/haproxy.service; enabled)
Active: active (running) since …
Main PID: 1234 (haproxy)
…
Reloaded: Thu 2026-06-22 14:03:12 UTC
- Inspect runtime logs for residual warnings:
journalctl -u haproxy -b | grep -i warning
No “invalid function call format” entries should appear.
Prevent – Guardrails for future deployments
- Schema validation step. Incorporate
haproxy -cinto every merge request pipeline before merging to main. - Template unit tests. Render templates with a matrix of environment variable values (including empty strings) and assert that the resulting file matches a regex pattern for
%\[[^\]]+\]. - Static analysis. Use tools like haproxy-lint (or a custom script) to detect unmatched parentheses/brackets.
- Explicit defaults. Always provide a fallback value for every environment‑derived variable used in HAProxy expressions.
- Version pinning. The syntax rules changed subtly between HAProxy 2.7 and 2.8; lock the binary version in CI to avoid surprise regressions.
Related Topic Hub: Distributed Systems Troubleshooting Hub
FAQ
- Why does the reload succeed locally but fail in CI? CI often runs the templating step with a different set of environment variables. Missing variables become empty strings, producing malformed
%[ ... ]expressions that pass local checks because the local file is hand‑edited. - Can I ignore the error and force a reload? No. HAProxy aborts the reload if the configuration does not parse. Forcing a reload would leave the previous configuration active, potentially serving stale routing rules.
- Is there a way to see which fetch caused the error without line numbers? Use
haproxy -c -f cfg -V(verbose mode). The output includes a tokenized view of each expression, helping locate the offending fetch. - Do converters like
str()require parentheses even when no arguments are needed? Yes. The syntax isconverter(arg); omitting the parentheses makes the token appear as an unknown fetch, triggering “invalid function call format”. - How do I handle dynamic backend selection safely? Store the backend name in a request‑scoped variable first, then use
use_backend %[var(req.backend)]. Ensure the variable is always set (e.g., default to a fallback backend) before theuse_backendline executes.