Hello Hyeonggeun,
On Tue, Jan 20, 2026 at 05:05:02PM +0900, Hyeonggeun Oh wrote:
> Hello Willy.
>
> Thanks for your careful comments and methods of handling changed in
> mail-based contribution.
>
> I've checked the issued you've caught and fixed a few lines.
> And also had many tests in various conditions.
>
> Please review my bug fixes.
Thank you! These indeed look better, but I'm still getting a crash
with ASAN mentioning a memory leak this time, only with very few
configs (I noticed it during regtests).
Running this config:
$ cat crash-asan2.cfg
global
stats socket /tmp/sock1 level admin mode 600
backend be
mode tcp
server www 127.0.0.1:80
$ ASAN_OPTIONS=abort_on_error=1:disable_coredump=0:unmap_shadow_on_exit=1 \
./haproxy -c -f crash-asan2.cfg
Causes this dump:
==12632==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 12 byte(s) in 2 object(s) allocated from:
#0 0x7fdf630b17ef in __interceptor_malloc
(/usr/lib64/libasan.so.6+0xb17ef)
#1 0x72a9a7 in my_strndup src/tools.c:3033
#2 0x9ffe5b in vars_check_arg src/vars.c:654
#3 0x788b5f in sample_parse_expr src/sample.c:1290
#4 0xb1612d in parse_acl_expr src/acl.c:232
#5 0xb1753b in parse_acl src/acl.c:671
#6 0xb1a006 in parse_acl_cond src/acl.c:920
#7 0xb1a30e in build_acl_cond src/acl.c:1021
#8 0xb283fc in parse_http_req_cond src/http_rules.c:149
#9 0x9bf24b in httpclient_resolve_init src/http_client.c:1076
#10 0x9cff8d in httpclient_create_proxy src/http_client.c:1207
#11 0x4ad699 in ssl_ocsp_update_precheck src/ssl_ocsp.c:1490
#12 0x4ad699 in ssl_ocsp_update_precheck src/ssl_ocsp.c:1482
#13 0x8e21f7 in step_init_2 src/haproxy.c:2105
#14 0x440bce in main src/haproxy.c:3391
#15 0x7fdf62a4503c in __libc_start_main (/lib64/libc.so.6+0x2403c)
(...)
We're getting closer though. I don't know if you've run regression tests
nor if you've run with ASAN. In order to enable ASAN, you need to append
ARCH_FLAGS="-g -fsanitize=address" to your "make" command line.
In order to run the regression tests, you'll need to build vtest2 (see
reg-tests/README for the trivial procedure), and run it:
$ ./scripts/run-regtests.sh reg-tests/startup/default_rules.vtc
It will create a report in /tmp/haregtests-something and the LOG file
will contain the output. It's a bit cryptic, but a line starting with
"----" indicates a fatal error and the cause generally is before. Here
we're seeing a line arboring "h14" (the name of a sub-test) with its
own config file that triggers the bug.
You can run it against all tests at once simply without passing any
test, and running up to one job per CPU (otherwise it takes ages):
$ ./scripts/run-regtests.sh --j $(nproc)
The output will indicate where failures are stored, by default successes
are removed so as to quickly spot failures in the temporary directory.
You may also be interested in writing you own test or completing an
existing one, to make sure nobody breaks your feature by accident in
the future. For example the test "reg-tests/sample_fetches/vars.vtc"
is very close to this, maybe adding one extra test inside for a dump
of all variables and one for the dump of a scope and prefix would be
useful and easy enough.
Regarding your patches, I've fixed a remaining leading tab on a lone line.
I suspect you're using emacs in auto-indent mode as your editor. They
totally broke auto-indent after version 24, it inserts spaces and tabs
on random lines after editing, it's a huge pain. And in addition when it
says it's disabled, it's not. You have to enable it again then disable
it to fix it. I still miss older versions. Mine is configured without
autoindent, and tab does it for me (I don't even think about it anymore).
In order to see such remaining spaces/tabs, I strongly suggest to enable
color mode as it allows to see invisible chars by acting on the background
color:
$ git config --global color.ui=true
Then if a "git diff" (before committing) or "git show" (after) shows some
red spaces, it means you have some end-of line spaces/tabs left that are
worth fixing.
I've also adjusted the doc to mention that by default when no scope is
specified, all scopes are iterated through in the described order, and
added a sentence and an example showing how to pass commas in arguments,
as it's not always trivial for everyone.
I've finally remerged them with the original ones. I'm attaching the
rebased series and I've temporarily pushed it as branch
"20260120-dump-vars-4a". For me the only issue left is this memory leak
that triggers the ASAN error. Once fixed I can merge the series.
Thank you for your efforts!
Willy
>From f8997b8c5a73da83d332373e6dbb04667fa95fd1 Mon Sep 17 00:00:00 2001
From: Hyeonggeun Oh <[email protected]>
Date: Tue, 13 Jan 2026 03:07:15 +0900
Subject: MINOR: tools: add chunk_escape_string() helper function
This function takes a string appends it to a buffer in a format
compatible with most languages (double-quoted, with special characters
escaped). It handles standard escape sequences like \n, \r, \", \\.
This generic utility is desined to be used for logging or debugging
purposes where arbitrary string data needs to be safely emitted without
breaking the output format. It will be primarily used by the upcoming
dump_all_vars() sample fetch to dump variable contents safely.
---
include/haproxy/tools.h | 7 ++++++
src/tools.c | 54 +++++++++++++++++++++++++++++++++++++++++
2 files changed, 61 insertions(+)
diff --git a/include/haproxy/tools.h b/include/haproxy/tools.h
index f181a76014..f84f02d7d6 100644
--- a/include/haproxy/tools.h
+++ b/include/haproxy/tools.h
@@ -466,6 +466,13 @@ char *escape_string(char *start, char *stop,
const char escape, const long *map,
const char *string, const char *string_stop);
+/*
+ * Appends a quoted and escaped string to a chunk buffer. The string is
+ * enclosed in double quotes and special characters are escaped with backslash.
+ * Returns 0 on success, -1 if the buffer is too small (output is rolled back).
+ */
+int chunk_escape_string(struct buffer *chunk, const char *str, size_t len);
+
/* Below are RFC8949 compliant cbor encode helper functions, see source
* file for functions descriptions
*/
diff --git a/src/tools.c b/src/tools.c
index 460a34e18f..9a4e1bf688 100644
--- a/src/tools.c
+++ b/src/tools.c
@@ -2129,6 +2129,60 @@ char *escape_string(char *start, char *stop,
return NULL;
}
+/*
+ * Appends a quoted and escaped string to a chunk buffer. The string is
+ * enclosed in double quotes and special characters are escaped with backslash:
+ * ", \, \r, \n, \b, \0
+ * Returns 0 on success, -1 if the buffer is too small (output is rolled back).
+ */
+int chunk_escape_string(struct buffer *chunk, const char *str, size_t len)
+{
+ size_t initial_data = chunk->data;
+ size_t i;
+
+ /* Opening quote */
+ if (chunk->data + 1 >= chunk->size)
+ return -1;
+ chunk->area[chunk->data++] = '"';
+
+ /* Escape and append each character */
+ for (i = 0; i < len; i++) {
+ unsigned char c = str[i];
+ const char *esc = NULL;
+
+ if (c == '"') esc = "\\\"";
+ else if (c == '\\') esc = "\\\\";
+ else if (c == '\r') esc = "\\r";
+ else if (c == '\n') esc = "\\n";
+ else if (c == '\b') esc = "\\b";
+ else if (c == '\0') esc = "\\0";
+
+ if (esc) {
+ if (chunk->data + 2 >= chunk->size) {
+ chunk->data = initial_data;
+ return -1;
+ }
+ chunk->area[chunk->data++] = esc[0];
+ chunk->area[chunk->data++] = esc[1];
+ } else {
+ if (chunk->data + 1 >= chunk->size) {
+ chunk->data = initial_data;
+ return -1;
+ }
+ chunk->area[chunk->data++] = c;
+ }
+ }
+
+ /* Closing quote */
+ if (chunk->data + 1 >= chunk->size) {
+ chunk->data = initial_data;
+ return -1;
+ }
+ chunk->area[chunk->data++] = '"';
+
+ return 0;
+}
+
/* CBOR helper to encode an uint64 value with prefix (3bits MAJOR type)
* according to RFC8949
*
--
2.35.3
>From 6e1ee4859c14246d40a636903e94d94780dba3ec Mon Sep 17 00:00:00 2001
From: Hyeonggeun Oh <[email protected]>
Date: Wed, 14 Jan 2026 03:20:27 +0900
Subject: MINOR: vars: store variable names for runtime access
Currently, variable names are only used during parsing and are not
stored at runtime. This makes it impossible to iterate through
variables and retrieve their names.
This patch adds infrastructure to store variable names:
- Add 'name' and 'name_len' fields to var_desc structure
- Add 'name' field to var structure
- Add VDF_NAME_ALLOCATED flag to track memory ownership
- Store names in vars_fill_desc(), var_set(), vars_check_arg(),
and parse_store()
- Free names in var_clear() and release_store_rule()
This prepares the ground for implementing dump_all_vars() in the
next commit.
---
include/haproxy/vars-t.h | 4 ++++
src/vars.c | 49 ++++++++++++++++++++++++++++++++++++++--
2 files changed, 51 insertions(+), 2 deletions(-)
diff --git a/include/haproxy/vars-t.h b/include/haproxy/vars-t.h
index 105506977c..24f6297435 100644
--- a/include/haproxy/vars-t.h
+++ b/include/haproxy/vars-t.h
@@ -57,12 +57,15 @@ struct vars {
};
#define VDF_PARENT_CTX 0x00000001 // Set if the variable is related to
the parent stream
+#define VDF_NAME_ALLOCATED 0x00000002 // Set if name was allocated and
must be freed
/* This struct describes a variable as found in an arg_data */
struct var_desc {
uint64_t name_hash;
enum vars_scope scope;
uint flags; /*VDF_* */
+ const char *name; /* variable name (not owned) */
+ size_t name_len; /* variable name length */
};
struct var {
@@ -70,6 +73,7 @@ struct var {
uint64_t name_hash; /* XXH3() of the variable's name, indexed by
<name_node> */
uint flags; // VF_*
/* 32-bit hole here */
+ char *name; /* variable name (allocated) */
struct sample_data data; /* data storage. */
};
diff --git a/src/vars.c b/src/vars.c
index 1755bf8ad1..b100c03f5e 100644
--- a/src/vars.c
+++ b/src/vars.c
@@ -178,6 +178,8 @@ unsigned int var_clear(struct vars *vars, struct var *var,
int force)
{
unsigned int size = 0;
+ ha_free(&var->name);
+
if (var->data.type == SMP_T_STR || var->data.type == SMP_T_BIN) {
ha_free(&var->data.u.str.area);
size += var->data.u.str.data;
@@ -322,6 +324,8 @@ static int vars_fill_desc(const char *name, int len, struct
var_desc *desc, char
}
desc->name_hash = XXH3(name, len, var_name_hash_seed);
+ desc->name = name;
+ desc->name_len = len;
return 1;
}
@@ -431,6 +435,16 @@ int var_set(const struct var_desc *desc, struct sample
*smp, uint flags)
goto unlock;
var->name_hash = desc->name_hash;
var->flags = flags & VF_PERMANENT;
+
+ /* Save variable name */
+ var->name = NULL;
+ if (desc->name && desc->name_len > 0) {
+ var->name = my_strndup(desc->name, desc->name_len);
+ if (!var->name) {
+ pool_free(var_pool, var);
+ goto unlock;
+ }
+ }
var->data.type = SMP_T_ANY;
cebu64_item_insert(&vars->name_root[var->name_hash %
VAR_NAME_ROOTS], name_node, name_hash, var);
}
@@ -623,6 +637,7 @@ int vars_check_arg(struct arg *arg, char **err)
{
struct sample empty_smp = { };
struct var_desc desc;
+ char *saved_name = NULL;
/* Check arg type. */
if (arg->type != ARGT_STR) {
@@ -634,15 +649,29 @@ int vars_check_arg(struct arg *arg, char **err)
if (!vars_fill_desc(arg->data.str.area, arg->data.str.data, &desc, err))
return 0;
- if (desc.scope == SCOPE_PROC && !var_set(&desc, &empty_smp,
VF_CREATEONLY|VF_PERMANENT))
+ /* Save variable name before destroying the chunk */
+ if (desc.name && desc.name_len > 0) {
+ saved_name = my_strndup(desc.name, desc.name_len);
+ if (!saved_name) {
+ memprintf(err, "out of memory");
+ return 0;
+ }
+
+ desc.name = saved_name;
+ }
+
+ if (desc.scope == SCOPE_PROC && !var_set(&desc, &empty_smp,
VF_CREATEONLY|VF_PERMANENT)) {
+ if (desc.flags & VDF_NAME_ALLOCATED)
+ ha_free(&saved_name);
return 0;
+ }
/* properly destroy the chunk */
chunk_destroy(&arg->data.str);
/* Use the global variable name pointer. */
arg->type = ARGT_VAR;
- arg->data.var = desc;
+ arg->data.var = desc; /* desc.name already points to saved_name */
return 1;
}
@@ -868,6 +897,10 @@ static void release_store_rule(struct act_rule *rule)
lf_expr_deinit(&rule->arg.vars.fmt);
release_sample_expr(rule->arg.vars.expr);
+
+ /* Free variable name if allocated */
+ if (rule->arg.vars.desc.flags & VDF_NAME_ALLOCATED)
+ ha_free((char **)&rule->arg.vars.desc.name);
}
/* This two function checks the variable name and replace the
@@ -919,6 +952,7 @@ static enum act_parse_ret parse_store(const char **args,
int *arg, struct proxy
struct ist condition = IST_NULL;
struct ist var = IST_NULL;
struct ist varname_ist = IST_NULL;
+ char *saved_name = NULL;
if (strncmp(var_name, "set-var-fmt", 11) == 0) {
var_name += 11;
@@ -973,6 +1007,17 @@ static enum act_parse_ret parse_store(const char **args,
int *arg, struct proxy
if (!vars_fill_desc(var_name, var_len, &rule->arg.vars.desc, err))
return ACT_RET_PRS_ERR;
+ /* Save variable name for runtime use */
+ if (rule->arg.vars.desc.name && rule->arg.vars.desc.name_len > 0) {
+ saved_name = my_strndup(rule->arg.vars.desc.name,
rule->arg.vars.desc.name_len);
+ if (!saved_name) {
+ memprintf(err, "out of memory");
+ return ACT_RET_PRS_ERR;
+ }
+ rule->arg.vars.desc.name = saved_name;
+ rule->arg.vars.desc.flags |= VDF_NAME_ALLOCATED;
+ }
+
if (rule->arg.vars.desc.scope == SCOPE_PROC &&
!var_set(&rule->arg.vars.desc, &empty_smp,
VF_CREATEONLY|VF_PERMANENT))
return 0;
--
2.35.3
>From 467cf99db1f3c73f378e173064b84c86f11c50df Mon Sep 17 00:00:00 2001
From: Hyeonggeun Oh <[email protected]>
Date: Tue, 13 Jan 2026 03:07:16 +0900
Subject: MINOR: vars: implement dump_all_vars() sample fetch
This patch implements dump_all_vars([scope],[prefix]) sample fetch
function that dumps all variables in a given scope, optionally
filtered by name prefix.
Output format: var1=value1, var2=value2, ...
- String values are quoted and escaped (", , \r, \n, \b, \0)
- All sample types are supported via sample_convert()
- Scope can be: sess, txn, req, res, proc
- Prefix filtering is optional
Example usage:
http-request return string %[dump_all_vars(txn)]
http-request return string %[dump_all_vars(txn,user)]
This addresses GitHub issue #1623.
---
src/vars.c | 251 +++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 251 insertions(+)
diff --git a/src/vars.c b/src/vars.c
index b100c03f5e..8cd0819ad9 100644
--- a/src/vars.c
+++ b/src/vars.c
@@ -58,6 +58,16 @@ static struct var_set_condition conditions_array[] = {
{ NULL, 0 }
};
+/* Variable scope names with their prefixes for output */
+static const char *var_scope_names[] = {
+ [SCOPE_SESS] = "sess.",
+ [SCOPE_TXN] = "txn.",
+ [SCOPE_REQ] = "req.",
+ [SCOPE_RES] = "res.",
+ [SCOPE_PROC] = "proc.",
+ [SCOPE_CHECK] = "check.",
+};
+
/* returns the struct vars pointer for a session, stream and scope, or NULL if
* it does not exist.
*/
@@ -352,6 +362,182 @@ static int smp_fetch_var(const struct arg *args, struct
sample *smp, const char
return vars_get_by_desc(var_desc, smp, def);
}
+/* Dumps all variables in the specified scope, optionally filtered by prefix.
+ * Output format: var1=value1, var2=value2, ...
+ * String values are quoted and escaped, binary values are hex-encoded (x...).
+ * Returns 1 on success, 0 on failure (buffer too small).
+ * Note: When using prefix filtering, all variables are still visited, so this
+ * should not be used with configs involving thousands of variables.
+ */
+static int smp_fetch_dump_all_vars(const struct arg *args, struct sample *smp,
const char *kw, void *private)
+{
+ struct buffer *output;
+ struct vars *vars;
+ struct var *var;
+ struct var_desc desc;
+ const char *prefix = NULL;
+ size_t prefix_len = 0;
+ const char *delim = ", ";
+ size_t delim_len = 2;
+ int first = 1;
+ int i;
+ int start_scope, end_scope;
+ int cur_scope;
+
+ /* Get output buffer */
+ output = get_trash_chunk();
+ chunk_reset(output);
+
+ /* Parse arguments */
+ if (args[0].type == ARGT_SINT) {
+ if (args[0].data.sint == -1) {
+ start_scope = SCOPE_SESS;
+ end_scope = SCOPE_PROC;
+ } else {
+ start_scope = end_scope = args[0].data.sint;
+ }
+ } else {
+ /* Auto-detect scope from context */
+ if (smp->strm)
+ start_scope = end_scope = SCOPE_TXN;
+ else if (smp->sess)
+ start_scope = end_scope = SCOPE_SESS;
+ else
+ start_scope = end_scope = SCOPE_PROC;
+ }
+
+ /* Optional prefix filter */
+ if (args[1].type == ARGT_STR) {
+ prefix = args[1].data.str.area;
+ prefix_len = args[1].data.str.data;
+ }
+
+ /* Optional delimiter */
+ if (args[2].type == ARGT_STR) {
+ delim = args[2].data.str.area;
+ delim_len = args[2].data.str.data;
+ }
+
+ desc.flags = 0;
+ desc.name_hash = 0;
+
+ for (cur_scope = start_scope; cur_scope <= end_scope; cur_scope++) {
+ desc.scope = cur_scope;
+
+ vars = get_vars(smp->sess, smp->strm, &desc);
+ if (!vars || vars->scope != desc.scope)
+ continue;
+
+ vars_rdlock(vars);
+
+ /* Iterate through all variable roots */
+ for (i = 0; i < VAR_NAME_ROOTS; i++) {
+ var = cebu64_item_first(&vars->name_root[i], name_node,
name_hash, struct var);
+
+ while (var) {
+ const char *scope_prefix;
+
+ /* Check prefix filter */
+ if (prefix) {
+ if (!var->name || strncmp(var->name,
prefix, prefix_len) != 0) {
+ var =
cebu64_item_next(&vars->name_root[i], name_node, name_hash, var);
+ continue;
+ }
+ }
+
+ /* Add delimiter */
+ if (!first) {
+ if (output->data + delim_len >=
output->size)
+ goto fail_unlock;
+ chunk_memcat(output, delim, delim_len);
+ }
+ first = 0;
+
+ /* Add variable name with scope prefix */
+ scope_prefix = var_scope_names[desc.scope];
+ if (var->name) {
+ if (chunk_appendf(output, "%s%s=",
scope_prefix, var->name) < 0)
+ goto fail_unlock;
+ } else {
+ if (chunk_appendf(output,
"var_%016llx=", (unsigned long long)var->name_hash) < 0)
+ goto fail_unlock;
+ }
+
+ /* Convert value based on type */
+ if (var->data.type == SMP_T_STR) {
+ /* String: quote and escape */
+ if (chunk_escape_string(output,
var->data.u.str.area, var->data.u.str.data) < 0)
+ goto fail_unlock;
+
+ } else if (var->data.type == SMP_T_BIN) {
+ /* Binary: hex encode */
+ if (dump_binary(output,
var->data.u.str.area, var->data.u.str.data) != var->data.u.str.data)
+ goto fail_unlock;
+ } else if (var->data.type == SMP_T_SINT) {
+ /* Integer */
+ if (chunk_appendf(output, "%lld", (long
long)var->data.u.sint) < 0)
+ goto fail_unlock;
+
+ } else if (var->data.type == SMP_T_BOOL) {
+ /* Boolean */
+ const char *bool_str = var->data.u.sint
? "true" : "false";
+ if (chunk_appendf(output, "%s",
bool_str) < 0)
+ goto fail_unlock;
+
+ } else if (var->data.type == SMP_T_IPV4 ||
var->data.type == SMP_T_IPV6) {
+ /* Address */
+ char addr_str[INET6_ADDRSTRLEN];
+ const char *res;
+
+ if (var->data.type == SMP_T_IPV4)
+ res = inet_ntop(AF_INET,
&var->data.u.ipv4, addr_str, sizeof(addr_str));
+ else
+ res = inet_ntop(AF_INET6,
&var->data.u.ipv6, addr_str, sizeof(addr_str));
+
+ if (!res) {
+ if (chunk_appendf(output,
"(addr)") < 0)
+ goto fail_unlock;
+ } else {
+ if (chunk_appendf(output, "%s",
addr_str) < 0)
+ goto fail_unlock;
+ }
+ } else if (var->data.type == SMP_T_METH) {
+ /* HTTP Method */
+ if (var->data.u.meth.meth ==
HTTP_METH_OTHER) {
+ if (chunk_escape_string(output,
var->data.u.meth.str.area, var->data.u.meth.str.data) < 0)
+ goto fail_unlock;
+ } else {
+ const char *method_str =
http_known_methods[var->data.u.meth.meth].ptr;
+ if (chunk_appendf(output,
"\"%s\"", method_str) < 0)
+ goto fail_unlock;
+ }
+ } else {
+ /* Other types: show type number */
+ if (chunk_appendf(output, "(type:%d)",
var->data.type) < 0)
+ goto fail_unlock;
+ }
+
+ var = cebu64_item_next(&vars->name_root[i],
name_node, name_hash, var);
+ }
+ }
+
+ vars_rdunlock(vars);
+ }
+
+ /* Set output sample */
+ smp->data.type = SMP_T_STR;
+ smp->data.u.str.area = output->area;
+ smp->data.u.str.data = output->data;
+ smp->flags &= ~SMP_F_CONST;
+
+ return 1;
+
+fail_unlock:
+ vars_rdunlock(vars);
+ output->data = 0;
+ return 0;
+}
+
/*
* Clear the contents of a variable so that it can be reset directly.
* This function is used just before a variable is filled out of a sample's
@@ -916,6 +1102,70 @@ static int smp_check_var(struct arg *args, char **err)
return vars_check_arg(&args[0], err);
}
+/* This function checks all arguments for dump_all_vars()
+ * Args: [scope], [prefix], [delimiter]
+ * Both arguments are optional
+ */
+static int smp_check_dump_all_vars(struct arg *args, char **err)
+{
+ /* First argument (scope) is optional */
+
+ if (args[0].type == ARGT_STR) {
+ const char *scope = args[0].data.str.area;
+ int scope_id = -1;
+ int i;
+ char buf[16];
+
+ if (args[0].data.str.data == 0) {
+ chunk_destroy(&args[0].data.str);
+ args[0].type = ARGT_SINT;
+ args[0].data.sint = scope_id;
+ return 1;
+ }
+
+ if (args[0].data.str.data < sizeof(buf) - 1) {
+ snprintf(buf, sizeof(buf), "%s.", scope);
+
+ for (i = 0; i <= SCOPE_CHECK; i++) {
+ if (strcmp(buf, var_scope_names[i]) == 0) {
+ scope_id = i;
+ break;
+ }
+ }
+ }
+
+ if (scope_id == -1) {
+ memprintf(err, "invalid scope '%s', must be one of:
sess, txn, req, res, proc", scope);
+ return 0;
+ }
+
+ chunk_destroy(&args[0].data.str);
+ args[0].type = ARGT_SINT;
+ args[0].data.sint = scope_id;
+ }
+ else if (args[0].type != ARGT_STOP) {
+ memprintf(err, "first argument must be a string (scope) or
omitted");
+ return 0;
+ } else {
+ args[0].type = ARGT_SINT;
+ args[0].data.sint = -1;
+ }
+
+ /* Second argument (prefix) is optional */
+ if (args[1].type != ARGT_STR && args[1].type != ARGT_STOP) {
+ memprintf(err, "second argument must be a string (prefix) or
omitted");
+ return 0;
+ }
+
+ /* Third argument (delimiter) is optional */
+ if (args[2].type != ARGT_STR && args[2].type != ARGT_STOP) {
+ memprintf(err, "third argument must be a string (delimiter) or
omitted");
+ return 0;
+ }
+
+ return 1;
+}
+
static int conv_check_var(struct arg *args, struct sample_conv *conv,
const char *file, int line, char **err_msg)
{
@@ -1410,6 +1660,7 @@ INITCALL0(STG_PREPARE, vars_init);
static struct sample_fetch_kw_list sample_fetch_keywords = {ILH, {
+ { "dump_all_vars", smp_fetch_dump_all_vars, ARG3(0,STR,STR,STR),
smp_check_dump_all_vars, SMP_T_STR, SMP_USE_CONST },
{ "var", smp_fetch_var, ARG2(1,STR,STR), smp_check_var, SMP_T_ANY,
SMP_USE_CONST },
{ /* END */ },
}};
--
2.35.3
>From 8963eab563f3620892c589770c25e0581f0ff5f3 Mon Sep 17 00:00:00 2001
From: Hyeonggeun Oh <[email protected]>
Date: Fri, 26 Dec 2025 15:57:35 +0900
Subject: DOC: vars: document dump_all_vars() sample fetch
Add documentation for the dump_all_vars() sample fetch function in the
configuration manual. This function was introduced in the previous commit
to dump all variables in a given scope with optional prefix filtering.
The documentation includes:
- Function signature and return type
- Description of output format
- Explanation of scope and prefix arguments
- Usage examples for common scenarios
This completes the implementation of GitHub issue #1623.
---
doc/configuration.txt | 65 +++++++++++++++++++++++++++++++++++++++++++
1 file changed, 65 insertions(+)
diff --git a/doc/configuration.txt b/doc/configuration.txt
index 20d9a5df64..d10c689720 100644
--- a/doc/configuration.txt
+++ b/doc/configuration.txt
@@ -23546,6 +23546,71 @@ var(<var-name>[,<default>]) : undefined
return it as a string. Empty strings are permitted. See section 2.8 about
variables for details.
+dump_all_vars([<scope>][,<prefix>][,<delimiter>]) : string
+ Returns a list of all variables in the specified scope, optionally filtered
+ by name prefix and with a customizable delimiter.
+
+ Output format: var1=value1<delim>var2=value2<delim>...
+
+ Value encoding by type:
+ - Strings: quoted and escaped (", \, \r, \n, \b, \0)
+ Example: txn.name="John \"Doe\""
+ - Binary: hex-encoded with 'x' prefix, unquoted
+ Example: txn.data=x48656c6c6f
+ - Integers: unquoted decimal
+ Example: txn.count=42
+ - Booleans: unquoted "true" or "false"
+ Example: txn.active=true
+ - Addresses: unquoted IP address string
+ Example: txn.client=192.168.1.1
+ - HTTP Methods: quoted string
+ Example: req.method="GET"
+
+ Arguments:
+ - <scope> (optional): sess, txn, req, res, or proc. If omitted, all these
+ scopes are visited in the same order as presented here.
+
+ - <prefix> (optional): filters variables whose names start with the
+ specified prefix (after removing the scope prefix).
+ Performance note: When using prefix filtering, all variables in the scope
+ are still visited. This should not be used with configurations involving
+ thousands of variables.
+
+ - <delimiter> (optional): string to separate variables. Defaults to ", "
+ (comma-space). Can be customized to any string. As a reminder, in order
+ to pass commas or spaces in a function argument, they need to be enclosed
+ in simple or double quotes (if the expression itself is already within
+ quotes, use the other ones).
+
+ Return value:
+ - On success: string containing all matching variables
+ - On failure: empty (sample fetch fails) if output buffer is too small.
+ The function will not truncate output; it fails completely to avoid
+ partial data.
+
+ This is particularly useful for debugging, logging, or exporting variable
+ states.
+
+ Examples:
+ # Dump all transaction variables
+ http-request return string %[dump_all_vars(txn)]
+
+ # Dump only variables starting with "user"
+ http-request set-header X-User-Vars "%[dump_all_vars(txn,user)]"
+
+ # Dump all process variables
+ http-request return string %[dump_all_vars(proc)]
+
+ # Custom delimiter (semicolon)
+ http-request set-header X-Vars "%[dump_all_vars(txn,,; )]"
+
+ # Force the default delimiter (comma space)
+ http-request set-header X-Vars "%[dump_all_vars(txn,,', ')]"
+
+ # Prefix filter with custom delimiter
+ http-request set-header X-Session "%[dump_all_vars(sess,user,|)]"
+
+
wait_end : boolean
This fetch either returns true when the inspection period is over, or does
not fetch. It is only used in ACLs, in conjunction with content analysis to
--
2.35.3