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

zwoop pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficserver.git


The following commit(s) were added to refs/heads/master by this push:
     new 2919403219 hrw4u: Add support for += / add-header (#12588)
2919403219 is described below

commit 2919403219f02cc450723a3bc43c38e11105c148
Author: Leif Hedstrom <[email protected]>
AuthorDate: Tue Oct 21 21:31:41 2025 -0700

    hrw4u: Add support for += / add-header (#12588)
---
 doc/admin-guide/configuration/hrw4u.en.rst         |  23 ++
 tools/hrw4u/grammar/hrw4u.g4                       |   2 +
 tools/hrw4u/src/common.py                          |   1 +
 tools/hrw4u/src/generators.py                      |  20 +-
 tools/hrw4u/src/hrw_symbols.py                     |  28 +-
 tools/hrw4u/src/kg_visitor.py                      |  33 +-
 tools/hrw4u/src/lsp/completions.py                 |  16 +-
 tools/hrw4u/src/lsp/hover.py                       |  43 ++-
 tools/hrw4u/src/suggestions.py                     |   9 +-
 tools/hrw4u/src/symbols.py                         |  82 +++--
 tools/hrw4u/src/symbols_base.py                    |  22 +-
 tools/hrw4u/src/tables.py                          | 350 ++++++---------------
 tools/hrw4u/src/types.py                           |  76 ++++-
 tools/hrw4u/src/visitor.py                         |  12 +
 tools/hrw4u/tests/data/hooks/remap.ast.txt         |   2 +-
 tools/hrw4u/tests/data/hooks/remap.input.txt       |   2 +
 tools/hrw4u/tests/data/hooks/remap.output.txt      |   2 +
 tools/hrw4u/tests/data/ops/exceptions.txt          |   2 +
 .../tests/data/ops/http_cntl_valid_bools.ast.txt   |   2 +-
 .../data/ops/http_cntl_valid_bools.output.txt      |   2 +-
 tools/hrw4u/tests/data/ops/qsa.output.txt          |   2 +-
 21 files changed, 370 insertions(+), 361 deletions(-)

diff --git a/doc/admin-guide/configuration/hrw4u.en.rst 
b/doc/admin-guide/configuration/hrw4u.en.rst
index 80beec6afe..39002b919d 100644
--- a/doc/admin-guide/configuration/hrw4u.en.rst
+++ b/doc/admin-guide/configuration/hrw4u.en.rst
@@ -231,6 +231,7 @@ The preference is the assignment style when appropriate.
 ============================= ================================= 
================================================
 Header Rewrite                HRW4U                             Description
 ============================= ================================= 
================================================
+add-header X-bar foo          inbound.{req,resp}.x-Bar += "bar" Add the header 
to (possibly) an existing header
 counter my_stat               counter("my_stat")                Increment 
internal counter
 rm-client-header X-Foo        inbound.req.X-Foo = ""            Remove a 
client request header
 rm-cookie foo                 {in,out}bound.cookie.foo = ""     Remove the 
cookie named foo
@@ -254,6 +255,28 @@ set-status-reason "No"        http.status.reason = "no"    
     Set the response
 set-http-cntl                 http.cntl.<C> = bool              Turn on/off 
<:ref:`C<admin-plugins-header-rewrite-set-http-cntl>`> controllers
 ============================= ================================= 
================================================
 
+Adding Headers with the += Operator
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+HRW4U provides a special ``+=`` operator for adding headers::
+
+    REMAP {
+        # Using += to add a header (maps to add-header)
+        inbound.req.X-Custom-Header += "new-value";
+    }
+
+The ``+=`` operator only works with the following pre-defined symbols:
+
+- ``inbound.req.<header>`` - Client request headers
+- ``inbound.resp.<header>`` - Origin response headers
+- ``outbound.req.<header>`` - Outbound request headers (context-restricted)
+- ``outbound.resp.<header>`` - Outbound response headers (context-restricted)
+
+.. note::
+    The ``+=`` operator differs from ``=`` in that ``=`` will replace/set the 
header value (mapping to
+    ``set-header``), while ``+=`` will add a new instance of the header 
(mapping to ``add-header``).
+    This is important for headers that can have multiple values, such as 
``Set-Cookie`` or custom headers.
+
 In addition to those operators above, HRW4U supports the following special 
operators without arguments:
 
 ================= ============================ ================================
diff --git a/tools/hrw4u/grammar/hrw4u.g4 b/tools/hrw4u/grammar/hrw4u.g4
index 852d72c056..a91e314336 100644
--- a/tools/hrw4u/grammar/hrw4u.g4
+++ b/tools/hrw4u/grammar/hrw4u.g4
@@ -69,6 +69,7 @@ LBRACKET      : '[';
 RBRACKET      : ']';
 EQUALS        : '==';
 EQUAL         : '=';
+PLUSEQUAL     : '+=';
 NEQ           : '!=';
 GT            : '>';
 LT            : '<';
@@ -127,6 +128,7 @@ statement
     : BREAK SEMICOLON
     | functionCall SEMICOLON
     | lhs=IDENT EQUAL value SEMICOLON
+    | lhs=IDENT PLUSEQUAL value SEMICOLON
     | op=IDENT SEMICOLON
     ;
 
diff --git a/tools/hrw4u/src/common.py b/tools/hrw4u/src/common.py
index c05a4ad40b..28478933c0 100644
--- a/tools/hrw4u/src/common.py
+++ b/tools/hrw4u/src/common.py
@@ -66,6 +66,7 @@ class SystemDefaults:
 class HeaderOperations:
     """Operation constants for various resource types"""
     OPERATIONS: Final = (MagicStrings.RM_HEADER.value, 
MagicStrings.SET_HEADER.value)
+    ADD_OPERATION: Final = MagicStrings.ADD_HEADER.value
     COOKIE_OPERATIONS: Final = (MagicStrings.RM_COOKIE.value, 
MagicStrings.SET_COOKIE.value)
     DESTINATION_OPERATIONS: Final = (MagicStrings.RM_DESTINATION.value, 
MagicStrings.SET_DESTINATION.value)
 
diff --git a/tools/hrw4u/src/generators.py b/tools/hrw4u/src/generators.py
index c5924b541e..31a547cd71 100644
--- a/tools/hrw4u/src/generators.py
+++ b/tools/hrw4u/src/generators.py
@@ -40,20 +40,22 @@ class TableGenerator:
         """Extract clean tag name from %{TAG:payload} format."""
         return tag.strip().removeprefix('%{').removesuffix('}').split(':')[0]
 
-    def generate_reverse_condition_map(self, condition_map: tuple[tuple[str, 
tuple], ...]) -> dict[str, str]:
+    def generate_reverse_condition_map(self, condition_map: tuple[tuple[str, 
Any], ...]) -> dict[str, str]:
         """Generate reverse condition mapping from forward condition map."""
         reverse_map = {}
 
-        for ident_key, (tag, _, _, _, _, _) in condition_map:
+        for ident_key, params in condition_map:
             if not ident_key.endswith('.'):
-                clean_tag = self._clean_tag(tag)
-                reverse_map[clean_tag] = ident_key
+                tag = params.target if params else None
+                if tag:
+                    clean_tag = self._clean_tag(tag)
+                    reverse_map[clean_tag] = ident_key
 
         return reverse_map
 
-    def generate_reverse_function_map(self, function_map: tuple[tuple[str, 
tuple], ...]) -> dict[str, str]:
+    def generate_reverse_function_map(self, function_map: tuple[tuple[str, 
Any], ...]) -> dict[str, str]:
         """Generate reverse function mapping from forward function map."""
-        return {tag: func_name for func_name, (tag, _) in function_map}
+        return {params.target: func_name for func_name, params in function_map}
 
     @cache
     def generate_section_hook_mapping(self) -> dict[str, str]:
@@ -70,7 +72,9 @@ class TableGenerator:
         from hrw4u.tables import CONDITION_MAP
 
         ip_mapping = {}
-        for condition_key, (tag, *_, reverse_info) in CONDITION_MAP.items():
+        for condition_key, params in CONDITION_MAP.items():
+            tag = params.target if params else None
+            reverse_info = params.rev if params else None
             if reverse_info and reverse_info.get("reverse_tag") == "IP":
                 payload = reverse_info.get("reverse_payload")
                 if payload:
@@ -162,7 +166,7 @@ def get_reverse_condition_map(condition_map: dict[str, 
tuple]) -> dict[str, str]
     return 
_table_generator.generate_reverse_condition_map(tuple(condition_map.items()))
 
 
-def get_reverse_function_map(function_map: dict[str, tuple]) -> dict[str, str]:
+def get_reverse_function_map(function_map: dict[str, Any]) -> dict[str, str]:
     """Get reverse function mapping."""
     return 
_table_generator.generate_reverse_function_map(tuple(function_map.items()))
 
diff --git a/tools/hrw4u/src/hrw_symbols.py b/tools/hrw4u/src/hrw_symbols.py
index b25049cff0..3487027977 100644
--- a/tools/hrw4u/src/hrw_symbols.py
+++ b/tools/hrw4u/src/hrw_symbols.py
@@ -42,8 +42,9 @@ class InverseSymbolResolver(SymbolResolverBase):
             return reverse_map
         # Fallback to building from condition map if not available
         result = {}
-        for ident_key, (tag, _, uppercase, *_) in self._condition_map.items():
+        for ident_key, params in self._condition_map.items():
             if not ident_key.endswith("."):
+                tag = params.target
                 tag_key = 
tag.strip().removeprefix("%{").removesuffix("}").split(":", 1)[0]
                 result[tag_key] = ident_key
         return result
@@ -55,8 +56,10 @@ class InverseSymbolResolver(SymbolResolverBase):
             return reverse_map
         # Fallback to building from condition map if not available
         result = []
-        for ident_key, (tag, _, uppercase, *_) in self._condition_map.items():
+        for ident_key, params in self._condition_map.items():
             if ident_key.endswith("."):
+                tag = params.target
+                uppercase = params.upper if params else False
                 result.append((tag, ident_key, uppercase))
         return result
 
@@ -65,7 +68,7 @@ class InverseSymbolResolver(SymbolResolverBase):
         """Cached reverse function mapping."""
         if reverse_map := tables.REVERSE_RESOLUTION_MAP.get('FUNCTIONS'):
             return reverse_map
-        return {tag: fn_name for fn_name, (tag, _) in 
self._function_map.items()}
+        return {params.target: fn_name for fn_name, params in 
self._function_map.items()}
 
     @cached_property
     def _rev_sections(self) -> dict[str, str]:
@@ -138,8 +141,10 @@ class InverseSymbolResolver(SymbolResolverBase):
         elif tag == "IP":
             return None
 
-        for key, (mapped_tag, _, _, restricted, _, _) in 
self._condition_map.items():
+        for key, params in self._condition_map.items():
+            mapped_tag = params.target
             tag_part = mapped_tag.replace("%{", "").replace("}", 
"").split(":")[0]
+            restricted = params.sections if params else None
             if tag_part == tag:
                 if not restricted or not section or section not in restricted:
                     pass
@@ -279,12 +284,15 @@ class InverseSymbolResolver(SymbolResolverBase):
 
             qargs = [status_code, 
self._rewrite_inline_percents(f'"{url_arg}"', section)]
         elif name == "add-header" and args:
+            # Convert add-header command to += syntax for reverse mapping
             header_name = args[0]
             prefix = self.get_prefix_for_context("header_ops", section)
             prefixed_header = f"{prefix}{header_name}"
 
-            processed_args = [self._rewrite_inline_percents(arg, section) for 
arg in args[1:]]
-            qargs = [prefixed_header] + processed_args
+            if len(args) > 1:
+                value = self._rewrite_inline_percents(args[1], section)
+                return f"{prefixed_header} += {value}"
+            raise SymbolResolutionError("add-header", "Missing value for 
add-header")
         elif name == "set-plugin-cntl" and len(args) >= 2:
             qualifier = args[0]
             value = args[1]
@@ -472,12 +480,14 @@ class InverseSymbolResolver(SymbolResolverBase):
                     rewritten_value = self._rewrite_inline_percents(value, 
section)
                 return f"{var_name} = {rewritten_value}"
 
-        for lhs_key, (commands, _, uppercase, _) in 
tables.OPERATOR_MAP.items():
+        for lhs_key, params in tables.OPERATOR_MAP.items():
+            commands = params.target if params else None
             if (isinstance(commands, (list, tuple)) and cmd in commands) or 
(cmd == commands):
+                uppercase = params.upper if params else False
                 return self._handle_operator_command(cmd, toks, lhs_key, 
uppercase, section)
 
-        for name, (forward_cmd, _) in tables.STATEMENT_FUNCTION_MAP.items():
-            if forward_cmd == cmd:
+        for name, params in tables.STATEMENT_FUNCTION_MAP.items():
+            if params.target == cmd:
                 return self._handle_statement_function(name, args, section, 
op_state)
 
         raise SymbolResolutionError(line, f"Unknown operator: {cmd}")
diff --git a/tools/hrw4u/src/kg_visitor.py b/tools/hrw4u/src/kg_visitor.py
index 78178dadeb..8b4b88929a 100644
--- a/tools/hrw4u/src/kg_visitor.py
+++ b/tools/hrw4u/src/kg_visitor.py
@@ -151,48 +151,55 @@ class KnowledgeGraphVisitor(hrw4uVisitor, BaseHRWVisitor):
                     "description": f"Apache Traffic Server hook for 
{hrw4u_section}"
                 }, f"ats_hook:{ats_hook}")
 
-        for op_pattern, (command, validator, uppercase, restricted_sections) 
in OPERATOR_MAP.items():
+        for op_pattern, params in OPERATOR_MAP.items():
+            validator = params.validate if params else None
+            restricted_sections = params.sections if params else None
+            command = params.target if params else None
             self._add_node(
                 "SemanticOperator", {
                     "pattern": op_pattern,
                     "hrw_operator": str(command) if isinstance(command, str) 
else str(command),
-                    "validates_uppercase": uppercase,
+                    "validates_uppercase": params.upper if params else False,
                     "has_validator": validator is not None,
                     "restricted_sections": [s.value for s in 
restricted_sections] if restricted_sections else None,
                     "description": f"Operator pattern {op_pattern} -> 
{command}"
                 }, f"sem_op:{op_pattern}")
 
-        for cond_pattern, (tag, validator, uppercase, restricted, 
default_expr, reverse_info) in CONDITION_MAP.items():
+        for cond_pattern, params in CONDITION_MAP.items():
+            validator = params.validate if params else None
+            restricted = params.sections if params else None
+            reverse_info = params.rev if params else None
+            tag = params.target if params else None
             self._add_node(
                 "SemanticCondition", {
                     "pattern": cond_pattern,
                     "hrw_condition": tag,
-                    "validates_uppercase": uppercase,
+                    "validates_uppercase": params.upper if params else False,
                     "has_validator": validator is not None,
                     "restricted_sections": [s.value for s in restricted] if 
restricted else None,
-                    "has_default_expression": default_expr,
+                    "has_default_expression": params.prefix if params else 
False,
                     "reverse_mapping": reverse_info,
                     "description": f"Condition pattern {cond_pattern} -> {tag}"
                 }, f"sem_cond:{cond_pattern}")
 
-        for func_name, (tag, validator) in FUNCTION_MAP.items():
+        for func_name, params in FUNCTION_MAP.items():
             self._add_node(
                 "SemanticFunction", {
                     "name": func_name,
-                    "hrw_condition": tag,
-                    "has_validator": validator is not None,
+                    "hrw_condition": params.target,
+                    "has_validator": params.validate is not None,
                     "type": "condition_function",
-                    "description": f"Function {func_name} -> %{{{tag}}}"
+                    "description": f"Function {func_name} -> 
%{{{params.target}}}"
                 }, f"sem_func:{func_name}")
 
-        for func_name, (command, validator) in STATEMENT_FUNCTION_MAP.items():
+        for func_name, params in STATEMENT_FUNCTION_MAP.items():
             self._add_node(
                 "SemanticFunction", {
                     "name": func_name,
-                    "hrw_operator": command,
-                    "has_validator": validator is not None,
+                    "hrw_operator": params.target,
+                    "has_validator": params.validate is not None,
                     "type": "statement_function",
-                    "description": f"Statement function {func_name} -> 
{command}"
+                    "description": f"Statement function {func_name} -> 
{params.target}"
                 }, f"sem_stmt_func:{func_name}")
 
         for suffix_group in SuffixGroup:
diff --git a/tools/hrw4u/src/lsp/completions.py 
b/tools/hrw4u/src/lsp/completions.py
index ab13ad6abc..1e8cf04197 100644
--- a/tools/hrw4u/src/lsp/completions.py
+++ b/tools/hrw4u/src/lsp/completions.py
@@ -224,18 +224,22 @@ class CompletionProvider:
         seen_labels = set()
 
         # Add condition completions
-        for key, (tag, _, _, sections, _, _) in tables.CONDITION_MAP.items():
+        for key, params in tables.CONDITION_MAP.items():
             if key.startswith(base_prefix) and key not in seen_labels:
                 seen_labels.add(key)
+                sections = params.sections if params else None
+                tag = params.target if params else None
 
                 item = self.builder.condition_completion(key, tag, sections, 
current_section, replacement_range)
                 if item:
                     completions.append(item.to_lsp_dict())
 
         # Add operator completions
-        for key, (commands, _, _, sections) in tables.OPERATOR_MAP.items():
+        for key, params in tables.OPERATOR_MAP.items():
             if key.startswith(base_prefix) and key not in seen_labels:
                 seen_labels.add(key)
+                sections = params.sections if params else None
+                commands = params.target if params else None
 
                 item = self.builder.operator_completion(key, commands, 
sections, current_section, replacement_range)
                 if item:
@@ -248,13 +252,13 @@ class CompletionProvider:
         completions = []
 
         # Regular functions
-        for func_name, (tag, _) in tables.FUNCTION_MAP.items():
-            item = self.builder.function_completion(func_name, tag, "Function")
+        for func_name, params in tables.FUNCTION_MAP.items():
+            item = self.builder.function_completion(func_name, params.target, 
"Function")
             completions.append(item.to_lsp_dict())
 
         # Statement functions
-        for func_name, (tag, _) in tables.STATEMENT_FUNCTION_MAP.items():
-            item = self.builder.function_completion(func_name, tag, 
"Statement")
+        for func_name, params in tables.STATEMENT_FUNCTION_MAP.items():
+            item = self.builder.function_completion(func_name, params.target, 
"Statement")
             completions.append(item.to_lsp_dict())
 
         return completions
diff --git a/tools/hrw4u/src/lsp/hover.py b/tools/hrw4u/src/lsp/hover.py
index 343fc2545d..6a6f6c92c5 100644
--- a/tools/hrw4u/src/lsp/hover.py
+++ b/tools/hrw4u/src/lsp/hover.py
@@ -403,8 +403,15 @@ class OperatorHoverProvider:
 
         # Check exact matches first
         if operator in OPERATOR_MAP:
-            commands, _, is_prefix, sections = OPERATOR_MAP[operator]
-            cmd_str = commands if isinstance(commands, str) else ' / 
'.join(commands)
+            params = OPERATOR_MAP[operator]
+            commands = params.target if params else None
+            if isinstance(commands, str):
+                cmd_str = commands
+            elif commands:
+                cmd_str = ' / '.join(commands)
+            else:
+                cmd_str = "unknown"
+            sections = params.sections if params else None
 
             section_info = ""
             if sections:
@@ -415,10 +422,17 @@ class OperatorHoverProvider:
                 f"**{operator}** - HRW4U Operator\n\n" + f"**Maps to:** 
`{cmd_str}`{section_info}")
 
         # Check prefix matches
-        for key, (commands, _, is_prefix, sections) in OPERATOR_MAP.items():
-            if is_prefix and operator.startswith(key):
-                cmd_str = commands if isinstance(commands, str) else ' / 
'.join(commands)
+        for key, params in OPERATOR_MAP.items():
+            if key.endswith('.') and operator.startswith(key):
+                commands = params.target if params else None
+                if isinstance(commands, str):
+                    cmd_str = commands
+                elif commands:
+                    cmd_str = ' / '.join(commands)
+                else:
+                    cmd_str = "unknown"
                 suffix = operator[len(key):]
+                sections = params.sections if params else None
 
                 section_info = ""
                 if sections:
@@ -431,7 +445,9 @@ class OperatorHoverProvider:
 
         # Check condition map
         if operator in CONDITION_MAP:
-            tag, _, is_prefix, sections, _, _ = CONDITION_MAP[operator]
+            params = CONDITION_MAP[operator]
+            tag = params.target if params else None
+            sections = params.sections if params else None
 
             section_info = ""
             if sections:
@@ -442,9 +458,11 @@ class OperatorHoverProvider:
                 f"**{operator}** - HRW4U Condition\n\n" + f"**Maps to:** 
`{tag}`{section_info}")
 
         # Check condition prefix matches
-        for key, (tag, _, is_prefix, sections, is_conditional, _) in 
CONDITION_MAP.items():
-            if is_prefix and operator.startswith(key):
+        for key, params in CONDITION_MAP.items():
+            if key.endswith('.') and operator.startswith(key):
+                tag = params.target if params else None
                 suffix = operator[len(key):]
+                sections = params.sections if params else None
 
                 section_info = ""
                 if sections:
@@ -581,14 +599,15 @@ class FunctionHoverProvider:
 
         # Fallback to basic documentation
         if function_name in FUNCTION_MAP:
-            tag, _ = FUNCTION_MAP[function_name]
+            params = FUNCTION_MAP[function_name]
             return HoverInfoProvider.create_hover_info(
-                f"**{function_name}()** - HRW4U Function\n\n" + f"**Maps to:** 
`{tag}`\n\n" + f"Used in conditional expressions.")
+                f"**{function_name}()** - HRW4U Function\n\n" + f"**Maps to:** 
`{params.target}`\n\n" +
+                f"Used in conditional expressions.")
 
         if function_name in STATEMENT_FUNCTION_MAP:
-            tag, _ = STATEMENT_FUNCTION_MAP[function_name]
+            params = STATEMENT_FUNCTION_MAP[function_name]
             return HoverInfoProvider.create_hover_info(
-                f"**{function_name}()** - HRW4U Statement Function\n\n" + 
f"**Maps to:** `{tag}`\n\n" +
+                f"**{function_name}()** - HRW4U Statement Function\n\n" + 
f"**Maps to:** `{params.target}`\n\n" +
                 f"Used as statements in code blocks.")
 
         return HoverInfoProvider.create_hover_info(f"**{function_name}()** - 
Unknown HRW4U function")
diff --git a/tools/hrw4u/src/suggestions.py b/tools/hrw4u/src/suggestions.py
index c1570e847a..13ce94c30b 100644
--- a/tools/hrw4u/src/suggestions.py
+++ b/tools/hrw4u/src/suggestions.py
@@ -85,11 +85,12 @@ class SuggestionEngine:
 
     def _is_symbol_valid_in_section(self, symbol: str, section: SectionType, 
context_type: str) -> bool:
         table_map = tables.OPERATOR_MAP if context_type == 'assignment' else 
tables.CONDITION_MAP
-        tuple_index = 3 if context_type == 'assignment' else 3
 
-        for key, data in table_map.items():
-            if (key == symbol or key == f"{symbol}.") and data[tuple_index]:
-                return section in data[tuple_index]
+        for key, params in table_map.items():
+            if key == symbol or key == f"{symbol}.":
+                sections = params.sections if params else None
+                if sections:
+                    return section in sections
 
         return True
 
diff --git a/tools/hrw4u/src/symbols.py b/tools/hrw4u/src/symbols.py
index 1d88305da8..a0f66de5fe 100644
--- a/tools/hrw4u/src/symbols.py
+++ b/tools/hrw4u/src/symbols.py
@@ -39,8 +39,8 @@ class SymbolResolver(SymbolResolverBase):
 
     def get_statement_spec(self, name: str) -> tuple[str, Callable[[str], 
None] | None]:
         # Use cached lookup from base class
-        if result := self._lookup_statement_function_cached(name):
-            return result
+        if params := self._lookup_statement_function_cached(name):
+            return params.target, params.validate
         raise SymbolResolutionError(name, "Unknown operator or invalid 
standalone use")
 
     def declare_variable(self, name: str, type_name: str) -> str:
@@ -63,32 +63,31 @@ class SymbolResolver(SymbolResolverBase):
 
     def resolve_assignment(self, name: str, value: str, section: SectionType | 
None = None) -> str:
         with self.debug_context("resolve_assignment", name, value, section):
-            for op_key, (commands, validator, uppercase, restricted_sections) 
in self._operator_map.items():
+            for op_key, params in self._operator_map.items():
                 if op_key.endswith("."):
                     if name.startswith(op_key):
-                        self.validate_section_access(name, section, 
restricted_sections)
+                        self.validate_section_access(name, section, 
params.sections if params else None)
                         qualifier = name[len(op_key):]
-                        if uppercase:
+                        if params and params.upper:
                             qualifier = qualifier.upper()
-                        if validator:
-                            validator(qualifier)
+                        if params and params.validate:
+                            params.validate(qualifier)
 
-                        # Add boolean value validation for http.cntl 
assignments.
+                        # Add boolean value validation for http.cntl 
assignments
                         if op_key == "http.cntl.":
                             types.SuffixGroup.BOOL_FIELDS.validate(value)
 
+                        commands = params.target if params else None
                         if isinstance(commands, (list, tuple)):
-                            if value == '""':
-                                return f"{commands[0]} {qualifier}"
-                            else:
-                                return f"{commands[1]} {qualifier} {value}"
-                        else:
-                            return f"{commands} {qualifier} {value}"
+                            return f"{commands[0 if value == '\"\"' else 1]} 
{qualifier}" + ("" if value == '""' else f" {value}")
+                        return f"{commands} {qualifier} {value}"
+
                 elif name == op_key:
-                    self.validate_section_access(name, section, 
restricted_sections)
-                    if validator:
-                        validator(value)
-                    return f"{commands} {value}"
+                    # Exact match - validate and return
+                    self.validate_section_access(name, section, 
params.sections if params else None)
+                    if params and params.validate:
+                        params.validate(value)
+                    return f"{params.target if params else None} {value}"
 
             if resolved_lhs := self.symbol_for(name):
                 if resolved_rhs := self.symbol_for(value):
@@ -105,26 +104,51 @@ class SymbolResolver(SymbolResolverBase):
                 error.add_symbol_suggestion(suggestions)
             raise error
 
+    def resolve_add_assignment(self, name: str, value: str, section: 
SectionType | None = None) -> str:
+        """Resolve += assignment, if it is supported for the given operator."""
+        with self.debug_context("resolve_add_assignment", name, value, 
section):
+            for op_key, params in self._operator_map.items():
+                if op_key.endswith(".") and name.startswith(op_key) and params 
and params.add:
+                    self.validate_section_access(name, section, 
params.sections)
+                    qualifier = name[len(op_key):]
+                    if params.validate:
+                        params.validate(qualifier)
+
+                    from hrw4u.common import HeaderOperations
+                    return f"{HeaderOperations.ADD_OPERATION} {qualifier} 
{value}"
+
+            # += not allowed if no matching operator with 'add' flag found
+            error = SymbolResolutionError(name, "+= operator is not supported 
for this assignment")
+            error.add_note("Only operators with 'add' flag support +=")
+            raise error
+
     def resolve_condition(self, name: str, section: SectionType | None = None) 
-> tuple[str, bool]:
         with self.debug_context("resolve_condition", name, section):
             if symbol := self.symbol_for(name):
                 return symbol.as_cond(), False
 
-            if condition_info := self._lookup_condition_cached(name):
-                tag, _, _, restricted, default_expr, _ = condition_info
+            if params := self._lookup_condition_cached(name):
+                tag = params.target if params else None
+                restricted = params.sections if params else None
                 self.validate_section_access(name, section, restricted)
-                return tag, default_expr
+                # For exact matches, default_expr is determined by whether 
it's a prefix pattern
+                return tag, False
 
             # Check prefix matches using base class utility
             prefix_matches = self.find_prefix_matches(name, 
self._condition_map)
-            for prefix, (tag, validator, uppercase, restricted, default_expr, 
_) in prefix_matches:
+            for prefix, params in prefix_matches:
+                tag = params.target if params else None
+                validator = params.validate if params else None
+                restricted = params.sections if params else None
+
                 self.validate_section_access(name, section, restricted)
                 suffix = name[len(prefix):]
-                suffix_norm = suffix.upper() if uppercase else suffix
+                suffix_norm = suffix.upper() if (params and params.upper) else 
suffix
                 if validator:
                     validator(suffix_norm)
                 resolved = f"%{{{tag}:{suffix_norm}}}"
-                return resolved, default_expr
+                # For prefix matches, default_expr is True (indicated by 
prefix flag)
+                return resolved, (params.prefix if params else False)
 
             error = SymbolResolutionError(name, "Unknown condition symbol")
             declared_vars = list(self._symbols.keys())
@@ -135,8 +159,9 @@ class SymbolResolver(SymbolResolverBase):
 
     def resolve_function(self, func_name: str, args: list[str], strip_quotes: 
bool = False) -> str:
         with self.debug_context("resolve_function", func_name, args):
-            if function_info := self._lookup_function_cached(func_name):
-                tag, validator = function_info
+            if params := self._lookup_function_cached(func_name):
+                tag = params.target
+                validator = params.validate
                 if validator:
                     validator(args)
 
@@ -156,8 +181,9 @@ class SymbolResolver(SymbolResolverBase):
 
     def resolve_statement_func(self, func_name: str, args: list[str]) -> str:
         with self.debug_context("resolve_statement_func", func_name, args):
-            if function_info := 
self._lookup_statement_function_cached(func_name):
-                command, validator = function_info
+            if params := self._lookup_statement_function_cached(func_name):
+                command = params.target
+                validator = params.validate
                 if validator:
                     validator(args)
 
diff --git a/tools/hrw4u/src/symbols_base.py b/tools/hrw4u/src/symbols_base.py
index c0103ea9a0..5749065074 100644
--- a/tools/hrw4u/src/symbols_base.py
+++ b/tools/hrw4u/src/symbols_base.py
@@ -38,22 +38,19 @@ class SymbolResolverBase:
 
     # Cached table access for performance - Python 3.11+ cached_property
     @cached_property
-    def _condition_map(
-            self) -> dict[str, tuple[str, Callable[[str], None] | None, bool, 
set[SectionType] | None, bool, dict | None]]:
+    def _condition_map(self) -> dict[str, types.MapParams]:
         return tables.CONDITION_MAP
 
     @cached_property
-    def _operator_map(
-            self
-    ) -> dict[str, tuple[str | list[str] | tuple[str, ...], Callable[[str], 
None] | None, bool, set[SectionType] | None]]:
+    def _operator_map(self) -> dict[str, types.MapParams]:
         return tables.OPERATOR_MAP
 
     @cached_property
-    def _function_map(self) -> dict[str, tuple[str, Callable[[list[str]], 
None] | None]]:
+    def _function_map(self) -> dict[str, types.MapParams]:
         return tables.FUNCTION_MAP
 
     @cached_property
-    def _statement_function_map(self) -> dict[str, tuple[str, 
Callable[[list[str]], None] | None]]:
+    def _statement_function_map(self) -> dict[str, types.MapParams]:
         return tables.STATEMENT_FUNCTION_MAP
 
     @cached_property
@@ -65,22 +62,19 @@ class SymbolResolverBase:
             raise SymbolResolutionError(name, f"{name} is not available in the 
{section.value} section")
 
     @lru_cache(maxsize=256)
-    def _lookup_condition_cached(
-            self, name: str) -> tuple[str, Callable[[str], None] | None, bool, 
set[SectionType] | None, bool, dict | None] | None:
+    def _lookup_condition_cached(self, name: str) -> types.MapParams | None:
         return self._condition_map.get(name)
 
     @lru_cache(maxsize=256)
-    def _lookup_operator_cached(
-            self, name: str
-    ) -> tuple[str | list[str] | tuple[str, ...], Callable[[str], None] | 
None, bool, set[SectionType] | None] | None:
+    def _lookup_operator_cached(self, name: str) -> types.MapParams | None:
         return self._operator_map.get(name)
 
     @lru_cache(maxsize=128)
-    def _lookup_function_cached(self, name: str) -> tuple[str, 
Callable[[list[str]], None] | None] | None:
+    def _lookup_function_cached(self, name: str) -> types.MapParams | None:
         return self._function_map.get(name)
 
     @lru_cache(maxsize=128)
-    def _lookup_statement_function_cached(self, name: str) -> tuple[str, 
Callable[[list[str]], None] | None] | None:
+    def _lookup_statement_function_cached(self, name: str) -> types.MapParams 
| None:
         return self._statement_function_map.get(name)
 
     def _debug_enter(self, method_name: str, *args: Any) -> None:
diff --git a/tools/hrw4u/src/tables.py b/tools/hrw4u/src/tables.py
index 7454f69276..85d93394ba 100644
--- a/tools/hrw4u/src/tables.py
+++ b/tools/hrw4u/src/tables.py
@@ -20,274 +20,102 @@ from typing import Final, Callable
 from dataclasses import dataclass
 from hrw4u.generators import get_complete_reverse_resolution_map
 from hrw4u.validation import Validator
-import hrw4u.types as types
+from hrw4u.types import MapParams, SuffixGroup
 from hrw4u.states import SectionType
 from hrw4u.common import HeaderOperations
 
-OPERATOR_MAP: dict[str, tuple[str | list[str] | tuple[str, ...], 
Callable[[str], None] | None, bool, set[SectionType] | None]] = {
-    "http.cntl.": ("set-http-cntl", 
Validator.suffix_group(types.SuffixGroup.HTTP_CNTL_FIELDS), True, None),
-    "http.status.reason": ("set-status-reason", Validator.quoted_or_simple(), 
False, None),
-    "http.status": ("set-status", Validator.range(0, 999), False, None),
-    "inbound.conn.dscp": ("set-conn-dscp", Validator.nbit_int(6), False, None),
-    "inbound.conn.mark": ("set-conn-mark", Validator.nbit_int(32), False, 
None),
-    "outbound.conn.dscp":
-        ("set-conn-dscp", Validator.nbit_int(6), False, 
{SectionType.PRE_REMAP, SectionType.REMAP, SectionType.READ_REQUEST}),
-    "outbound.conn.mark":
-        ("set-conn-mark", Validator.nbit_int(32), False, 
{SectionType.PRE_REMAP, SectionType.REMAP, SectionType.READ_REQUEST}),
-    "inbound.cookie.": (HeaderOperations.COOKIE_OPERATIONS, 
Validator.http_token(), False, None),
-    "inbound.req.": (HeaderOperations.OPERATIONS, 
Validator.http_header_name(), False, None),
-    "inbound.resp.body": ("set-body", Validator.quoted_or_simple(), False, 
None),
-    "inbound.resp.": (HeaderOperations.OPERATIONS, 
Validator.http_header_name(), False, None),
-    "inbound.status.reason": ("set-status-reason", 
Validator.quoted_or_simple(), False, None),
-    "inbound.status": ("set-status", Validator.range(0, 999), False, None),
-    "inbound.url.": (HeaderOperations.DESTINATION_OPERATIONS, 
Validator.suffix_group(types.SuffixGroup.URL_FIELDS), True, None),
-    "outbound.cookie.":
-        (
-            HeaderOperations.COOKIE_OPERATIONS, Validator.http_token(), False,
-            {SectionType.PRE_REMAP, SectionType.REMAP, 
SectionType.READ_REQUEST}),
-    "outbound.req.":
-        (
-            HeaderOperations.OPERATIONS, Validator.http_header_name(), False,
-            {SectionType.PRE_REMAP, SectionType.REMAP, 
SectionType.READ_REQUEST}),
-    "outbound.resp.":
-        (
-            HeaderOperations.OPERATIONS, Validator.http_header_name(), False,
-            {SectionType.PRE_REMAP, SectionType.REMAP, 
SectionType.READ_REQUEST, SectionType.SEND_REQUEST}),
-    "outbound.status.reason":
-        (
-            "set-status-reason", Validator.quoted_or_simple(), False,
-            {SectionType.PRE_REMAP, SectionType.REMAP, 
SectionType.READ_REQUEST, SectionType.SEND_REQUEST}),
-    "outbound.status":
-        (
-            "set-status", Validator.range(0, 999), False,
-            {SectionType.PRE_REMAP, SectionType.REMAP, 
SectionType.READ_REQUEST, SectionType.SEND_REQUEST}),
-    "outbound.url.":
-        (
-            HeaderOperations.DESTINATION_OPERATIONS, 
Validator.suffix_group(types.SuffixGroup.URL_FIELDS), True,
-            {SectionType.PRE_REMAP, SectionType.REMAP, 
SectionType.READ_REQUEST})
+# yapf: disable
+OPERATOR_MAP: dict[str, MapParams] = {
+    "http.cntl.": MapParams(target="set-http-cntl", upper=True, 
validate=Validator.suffix_group(SuffixGroup.HTTP_CNTL_FIELDS)),
+    "http.status.reason": MapParams(target="set-status-reason", 
validate=Validator.quoted_or_simple()),
+    "http.status": MapParams(target="set-status", validate=Validator.range(0, 
999)),
+    "inbound.conn.dscp": MapParams(target="set-conn-dscp", 
validate=Validator.nbit_int(6)),
+    "inbound.conn.mark": MapParams(target="set-conn-mark", 
validate=Validator.nbit_int(32)),
+    "outbound.conn.dscp": MapParams(target="set-conn-dscp", 
validate=Validator.nbit_int(6), sections={SectionType.PRE_REMAP, 
SectionType.REMAP, SectionType.READ_REQUEST}),
+    "outbound.conn.mark": MapParams(target="set-conn-mark", 
validate=Validator.nbit_int(32), sections={SectionType.PRE_REMAP, 
SectionType.REMAP, SectionType.READ_REQUEST}),
+    "inbound.cookie.": MapParams(target=HeaderOperations.COOKIE_OPERATIONS, 
validate=Validator.http_token()),
+    "inbound.req.": MapParams(target=HeaderOperations.OPERATIONS, add=True, 
validate=Validator.http_header_name()),
+    "inbound.resp.body": MapParams(target="set-body", 
validate=Validator.quoted_or_simple()),
+    "inbound.resp.": MapParams(target=HeaderOperations.OPERATIONS, add=True, 
validate=Validator.http_header_name()),
+    "inbound.status.reason": MapParams(target="set-status-reason", 
validate=Validator.quoted_or_simple()),
+    "inbound.status": MapParams(target="set-status", 
validate=Validator.range(0, 999)),
+    "inbound.url.": MapParams(target=HeaderOperations.DESTINATION_OPERATIONS, 
upper=True, validate=Validator.suffix_group(SuffixGroup.URL_FIELDS)),
+    "outbound.cookie.": MapParams(target=HeaderOperations.COOKIE_OPERATIONS, 
validate=Validator.http_token(), sections={SectionType.PRE_REMAP, 
SectionType.REMAP, SectionType.READ_REQUEST}),
+    "outbound.req.": MapParams(target=HeaderOperations.OPERATIONS, add=True, 
validate=Validator.http_header_name(), sections={SectionType.PRE_REMAP, 
SectionType.REMAP, SectionType.READ_REQUEST}),
+    "outbound.resp.": MapParams(target=HeaderOperations.OPERATIONS, add=True, 
validate=Validator.http_header_name(), sections={SectionType.PRE_REMAP, 
SectionType.REMAP, SectionType.READ_REQUEST, SectionType.SEND_REQUEST}),
+    "outbound.status.reason": MapParams(target="set-status-reason", 
validate=Validator.quoted_or_simple(), sections={SectionType.PRE_REMAP, 
SectionType.REMAP, SectionType.READ_REQUEST, SectionType.SEND_REQUEST}),
+    "outbound.status": MapParams(target="set-status", 
validate=Validator.range(0, 999), sections={SectionType.PRE_REMAP, 
SectionType.REMAP, SectionType.READ_REQUEST, SectionType.SEND_REQUEST}),
+    "outbound.url.": MapParams(target=HeaderOperations.DESTINATION_OPERATIONS, 
upper=True, validate=Validator.suffix_group(SuffixGroup.URL_FIELDS), 
sections={SectionType.PRE_REMAP, SectionType.REMAP, SectionType.READ_REQUEST})
 }
 
-STATEMENT_FUNCTION_MAP: dict[str, tuple[str, Callable[[list[str]], None] | 
None]] = {
-    "add-header":
-        ("add-header", Validator.arg_count(2).arg_at(0, 
Validator.http_header_name()).arg_at(1, Validator.quoted_or_simple())),
-    "counter": ("counter", Validator.arg_count(1).quoted_or_simple()),
-    "set-debug": ("set-debug", Validator.arg_count(0)),
-    "no-op": ("no-op", Validator.arg_count(0)),
-    "remove_query": ("rm-destination QUERY", 
Validator.arg_count(1).quoted_or_simple()),
-    "keep_query": ("rm-destination QUERY", 
Validator.arg_count(1).quoted_or_simple()),
-    "run-plugin": ("run-plugin", Validator.min_args(1).quoted_or_simple()),
-    "set-body-from": ("set-body-from", 
Validator.arg_count(1).quoted_or_simple()),
-    "set-config": ("set-config", Validator.arg_count(2).quoted_or_simple()),
-    "set-redirect":
-        ("set-redirect", Validator.arg_count(2).arg_at(0, Validator.range(300, 
399)).arg_at(1, Validator.quoted_or_simple())),
-    "skip-remap":
-        ("skip-remap", 
Validator.arg_count(1).suffix_group(types.SuffixGroup.BOOL_FIELDS)._add(Validator.normalize_arg_at(0))),
-    "set-plugin-cntl":
-        (
-            "set-plugin-cntl", 
Validator.arg_count(2)._add(Validator.normalize_arg_at(0)).arg_at(
-                0, 
Validator.suffix_group(types.SuffixGroup.PLUGIN_CNTL_FIELDS))._add(Validator.normalize_arg_at(1))._add(
-                    
Validator.conditional_arg_validation(types.SuffixGroup.PLUGIN_CNTL_MAPPING.value))),
+STATEMENT_FUNCTION_MAP: dict[str, MapParams] = {
+    "add-header": MapParams(target="add-header", 
validate=Validator.arg_count(2).arg_at(0, 
Validator.http_header_name()).arg_at(1, Validator.quoted_or_simple())),
+    "counter": MapParams(target="counter", 
validate=Validator.arg_count(1).quoted_or_simple()),
+    "set-debug": MapParams(target="set-debug", 
validate=Validator.arg_count(0)),
+    "no-op": MapParams(target="no-op", validate=Validator.arg_count(0)),
+    "remove_query": MapParams(target="rm-destination QUERY", 
validate=Validator.arg_count(1).quoted_or_simple()),
+    "keep_query": MapParams(target="rm-destination QUERY", 
validate=Validator.arg_count(1).quoted_or_simple()),
+    "run-plugin": MapParams(target="run-plugin", 
validate=Validator.min_args(1).quoted_or_simple()),
+    "set-body-from": MapParams(target="set-body-from", 
validate=Validator.arg_count(1).quoted_or_simple()),
+    "set-config": MapParams(target="set-config", 
validate=Validator.arg_count(2).quoted_or_simple()),
+    "set-redirect": MapParams(target="set-redirect", 
validate=Validator.arg_count(2).arg_at(0, Validator.range(300, 399)).arg_at(1, 
Validator.quoted_or_simple())),
+    "skip-remap": MapParams(target="skip-remap", 
validate=Validator.arg_count(1).suffix_group(SuffixGroup.BOOL_FIELDS)._add(Validator.normalize_arg_at(0))),
+    "set-plugin-cntl": MapParams(target="set-plugin-cntl", 
validate=Validator.arg_count(2)._add(Validator.normalize_arg_at(0)).arg_at(0, 
Validator.suffix_group(SuffixGroup.PLUGIN_CNTL_FIELDS))._add(Validator.normalize_arg_at(1))._add(Validator.conditional_arg_validation(SuffixGroup.PLUGIN_CNTL_MAPPING.value))),
 }
 
-FUNCTION_MAP = {
-    "access": ("ACCESS", Validator.arg_count(1).quoted_or_simple()),
-    "cache": ("CACHE", Validator.arg_count(0)),
-    "cidr": ("CIDR", Validator.arg_count(2).arg_at(0, Validator.range(1, 
32)).arg_at(1, Validator.range(1, 128))),
-    "internal": ("INTERNAL-TRANSACTION", Validator.arg_count(0)),
-    "random": ("RANDOM", Validator.arg_count(1).nbit_int(32)),
-    "ssn-txn-count": ("SSN-TXN-COUNT", Validator.arg_count(0)),
-    "txn-count": ("TXN-COUNT", Validator.arg_count(0)),
+FUNCTION_MAP: dict[str, MapParams] = {
+    "access": MapParams(target="ACCESS", 
validate=Validator.arg_count(1).quoted_or_simple()),
+    "cache": MapParams(target="CACHE", validate=Validator.arg_count(0)),
+    "cidr": MapParams(target="CIDR", validate=Validator.arg_count(2).arg_at(0, 
Validator.range(1, 32)).arg_at(1, Validator.range(1, 128))),
+    "internal": MapParams(target="INTERNAL-TRANSACTION", 
validate=Validator.arg_count(0)),
+    "random": MapParams(target="RANDOM", 
validate=Validator.arg_count(1).nbit_int(32)),
+    "ssn-txn-count": MapParams(target="SSN-TXN-COUNT", 
validate=Validator.arg_count(0)),
+    "txn-count": MapParams(target="TXN-COUNT", 
validate=Validator.arg_count(0)),
 }
 
-CONDITION_MAP: dict[str, tuple[str, Callable[[str], None] | None, bool, 
set[SectionType] | None, bool, dict | None]] = {
+CONDITION_MAP: dict[str, MapParams] = {
     # Exact matches with reverse mapping info
-    "inbound.ip": ("%{IP:CLIENT}", None, False, None, False, {
-        "reverse_tag": "IP",
-        "reverse_payload": "CLIENT"
-    }),
-    "inbound.method": ("%{METHOD}", None, False, None, False, {
-        "reverse_tag": "METHOD",
-        "ambiguous": True
-    }),
-    "inbound.server": ("%{IP:INBOUND}", None, False, None, False, {
-        "reverse_tag": "IP",
-        "reverse_payload": "INBOUND"
-    }),
-    "inbound.status": ("%{STATUS}", None, False, None, False, {
-        "reverse_tag": "STATUS",
-        "ambiguous": True
-    }),
-    "now": ("%{NOW}", None, False, None, False, None),
-    "outbound.ip":
-        (
-            "%{IP:SERVER}",
-            None,
-            False,
-            {SectionType.PRE_REMAP, SectionType.REMAP, 
SectionType.READ_REQUEST},
-            False,
-            {
-                "reverse_tag": "IP",
-                "reverse_payload": "SERVER"
-            },
-        ),
-    "outbound.method":
-        (
-            "%{METHOD}",
-            None,
-            False,
-            {SectionType.PRE_REMAP, SectionType.REMAP, 
SectionType.READ_REQUEST},
-            False,
-            {
-                "reverse_tag": "METHOD",
-                "ambiguous": True
-            },
-        ),
-    "outbound.server":
-        (
-            "%{IP:OUTBOUND}",
-            None,
-            False,
-            {SectionType.PRE_REMAP, SectionType.REMAP, 
SectionType.READ_REQUEST},
-            False,
-            {
-                "reverse_tag": "IP",
-                "reverse_payload": "OUTBOUND"
-            },
-        ),
-    "outbound.status":
-        (
-            "%{STATUS}",
-            None,
-            False,
-            {SectionType.PRE_REMAP, SectionType.REMAP, 
SectionType.READ_REQUEST, SectionType.SEND_REQUEST},
-            False,
-            {
-                "reverse_tag": "STATUS",
-                "ambiguous": True
-            },
-        ),
-    "tcp.info": ("%{TCP-INFO}", None, False, None, False, None),
+    "inbound.ip": MapParams(target="%{IP:CLIENT}", rev={"reverse_tag": "IP", 
"reverse_payload": "CLIENT"}),
+    "inbound.method": MapParams(target="%{METHOD}", rev={"reverse_tag": 
"METHOD", "ambiguous": True}),
+    "inbound.server": MapParams(target="%{IP:INBOUND}", rev={"reverse_tag": 
"IP", "reverse_payload": "INBOUND"}),
+    "inbound.status": MapParams(target="%{STATUS}", rev={"reverse_tag": 
"STATUS", "ambiguous": True}),
+    "now": MapParams(target="%{NOW}"),
+    "outbound.ip": MapParams(target="%{IP:SERVER}", 
sections={SectionType.PRE_REMAP, SectionType.REMAP, SectionType.READ_REQUEST}, 
rev={"reverse_tag": "IP", "reverse_payload": "SERVER"}),
+    "outbound.method": MapParams(target="%{METHOD}", 
sections={SectionType.PRE_REMAP, SectionType.REMAP, SectionType.READ_REQUEST}, 
rev={"reverse_tag": "METHOD", "ambiguous": True}),
+    "outbound.server": MapParams(target="%{IP:OUTBOUND}", 
sections={SectionType.PRE_REMAP, SectionType.REMAP, SectionType.READ_REQUEST}, 
rev={"reverse_tag": "IP", "reverse_payload": "OUTBOUND"}),
+    "outbound.status": MapParams(target="%{STATUS}", 
sections={SectionType.PRE_REMAP, SectionType.REMAP, SectionType.READ_REQUEST, 
SectionType.SEND_REQUEST}, rev={"reverse_tag": "STATUS", "ambiguous": True}),
+    "tcp.info": MapParams(target="%{TCP-INFO}"),
 
-    # Prefix matches with reverse mapping info
-    "capture.": ("LAST-CAPTURE", Validator.range(0, 9), False, None, True, 
None),
-    "from.url.": ("FROM-URL", 
Validator.suffix_group(types.SuffixGroup.URL_FIELDS), True, None, True, None),
-    "geo.": ("GEO", Validator.suffix_group(types.SuffixGroup.GEO_FIELDS), 
True, None, True, None),
-    "http.cntl.": ("HTTP-CNTL", 
Validator.suffix_group(types.SuffixGroup.HTTP_CNTL_FIELDS), True, None, False, 
None),
-    "id.": ("ID", Validator.suffix_group(types.SuffixGroup.ID_FIELDS), True, 
None, False, None),
-    "inbound.conn.client-cert.SAN.":
-        ("INBOUND:CLIENT-CERT:SAN", 
Validator.suffix_group(types.SuffixGroup.SAN_FIELDS), True, None, True, None),
-    "inbound.conn.server-cert.SAN.":
-        ("INBOUND:SERVER-CERT:SAN", 
Validator.suffix_group(types.SuffixGroup.SAN_FIELDS), True, None, True, None),
-    "inbound.conn.client-cert.san.":
-        ("INBOUND:CLIENT-CERT:SAN", 
Validator.suffix_group(types.SuffixGroup.SAN_FIELDS), True, None, True, None),
-    "inbound.conn.server-cert.san.":
-        ("INBOUND:SERVER-CERT:SAN", 
Validator.suffix_group(types.SuffixGroup.SAN_FIELDS), True, None, True, None),
-    "inbound.conn.client-cert.":
-        ("INBOUND:CLIENT-CERT", 
Validator.suffix_group(types.SuffixGroup.CERT_FIELDS), True, None, True, None),
-    "inbound.conn.server-cert.":
-        ("INBOUND:SERVER-CERT", 
Validator.suffix_group(types.SuffixGroup.CERT_FIELDS), True, None, True, None),
-    "inbound.conn.": ("INBOUND", 
Validator.suffix_group(types.SuffixGroup.CONN_FIELDS), True, None, True, None),
-    "inbound.cookie.": ("COOKIE", Validator.http_token(), False, None, True, {
-        "reverse_fallback": "inbound.cookie."
-    }),
-    "inbound.req.": ("CLIENT-HEADER", Validator.http_header_name(), False, 
None, True, {
-        "reverse_fallback": "inbound.req."
-    }),
-    "inbound.resp.": ("HEADER", Validator.http_header_name(), False, None, 
True, {
-        "reverse_context": "header_condition"
-    }),
-    "inbound.url.": ("CLIENT-URL", 
Validator.suffix_group(types.SuffixGroup.URL_FIELDS), True, None, True, None),
-    "now.": ("NOW", Validator.suffix_group(types.SuffixGroup.DATE_FIELDS), 
True, None, False, None),
-    "outbound.conn.client-cert.SAN.":
-        (
-            "OUTBOUND:CLIENT-CERT:SAN",
-            Validator.suffix_group(types.SuffixGroup.SAN_FIELDS),
-            True,
-            {SectionType.PRE_REMAP, SectionType.REMAP, 
SectionType.READ_REQUEST},
-            True,
-            None,
-        ),
-    "outbound.conn.server-cert.SAN.":
-        (
-            "OUTBOUND:SERVER-CERT:SAN",
-            Validator.suffix_group(types.SuffixGroup.SAN_FIELDS),
-            True,
-            {SectionType.PRE_REMAP, SectionType.REMAP, 
SectionType.READ_REQUEST},
-            True,
-            None,
-        ),
-    "outbound.conn.client-cert.san.":
-        (
-            "OUTBOUND:CLIENT-CERT:SAN",
-            Validator.suffix_group(types.SuffixGroup.SAN_FIELDS),
-            True,
-            {SectionType.PRE_REMAP, SectionType.REMAP, 
SectionType.READ_REQUEST},
-            True,
-            None,
-        ),
-    "outbound.conn.server-cert.san.":
-        (
-            "OUTBOUND:SERVER-CERT:SAN",
-            Validator.suffix_group(types.SuffixGroup.SAN_FIELDS),
-            True,
-            {SectionType.PRE_REMAP, SectionType.REMAP, 
SectionType.READ_REQUEST},
-            True,
-            None,
-        ),
-    "outbound.conn.client-cert.":
-        (
-            "OUTBOUND:CLIENT-CERT",
-            Validator.suffix_group(types.SuffixGroup.CERT_FIELDS),
-            True,
-            {SectionType.PRE_REMAP, SectionType.REMAP, 
SectionType.READ_REQUEST},
-            True,
-            None,
-        ),
-    "outbound.conn.server-cert.":
-        (
-            "OUTBOUND:SERVER-CERT",
-            Validator.suffix_group(types.SuffixGroup.CERT_FIELDS),
-            True,
-            {SectionType.PRE_REMAP, SectionType.REMAP, 
SectionType.READ_REQUEST},
-            True,
-            None,
-        ),
-    "outbound.conn.":
-        (
-            "OUTBOUND", Validator.suffix_group(types.SuffixGroup.CONN_FIELDS), 
True,
-            {SectionType.PRE_REMAP, SectionType.REMAP, 
SectionType.READ_REQUEST}, True, None),
-    "outbound.cookie.":
-        (
-            "COOKIE", Validator.http_token(), False, {SectionType.PRE_REMAP, 
SectionType.REMAP, SectionType.READ_REQUEST}, True, {
-                "reverse_fallback": "inbound.cookie."
-            }),
-    "outbound.req.":
-        (
-            "HEADER", Validator.http_header_name(), False, 
{SectionType.PRE_REMAP, SectionType.REMAP,
-                                                            
SectionType.READ_REQUEST}, True, {
-                                                                
"reverse_context": "header_condition"
-                                                            }),
-    "outbound.resp.":
-        (
-            "HEADER",
-            Validator.http_header_name(),
-            False,
-            {SectionType.PRE_REMAP, SectionType.REMAP, 
SectionType.READ_REQUEST, SectionType.SEND_REQUEST},
-            True,
-            {
-                "reverse_context": "header_condition"
-            },
-        ),
-    "outbound.url.":
-        (
-            "NEXT-HOP",
-            Validator.suffix_group(types.SuffixGroup.URL_FIELDS),
-            True,
-            {SectionType.PRE_REMAP, SectionType.REMAP, 
SectionType.READ_REQUEST},
-            True,
-            None,
-        ),
-    "to.url.": ("TO-URL", 
Validator.suffix_group(types.SuffixGroup.URL_FIELDS), True, None, True, None),
+    # Prefix matches
+    "capture.": MapParams(target="LAST-CAPTURE", prefix=True, 
validate=Validator.range(0, 9)),
+    "from.url.": MapParams(target="FROM-URL", upper=True, prefix=True, 
validate=Validator.suffix_group(SuffixGroup.URL_FIELDS)),
+    "geo.": MapParams(target="GEO", upper=True, prefix=True, 
validate=Validator.suffix_group(SuffixGroup.GEO_FIELDS)),
+    "http.cntl.": MapParams(target="HTTP-CNTL", upper=True, 
validate=Validator.suffix_group(SuffixGroup.HTTP_CNTL_FIELDS)),
+    "id.": MapParams(target="ID", upper=True, 
validate=Validator.suffix_group(SuffixGroup.ID_FIELDS)),
+    "inbound.conn.client-cert.SAN.": 
MapParams(target="INBOUND:CLIENT-CERT:SAN", upper=True, prefix=True, 
validate=Validator.suffix_group(SuffixGroup.SAN_FIELDS)),
+    "inbound.conn.server-cert.SAN.": 
MapParams(target="INBOUND:SERVER-CERT:SAN", upper=True, prefix=True, 
validate=Validator.suffix_group(SuffixGroup.SAN_FIELDS)),
+    "inbound.conn.client-cert.san.": 
MapParams(target="INBOUND:CLIENT-CERT:SAN", upper=True, prefix=True, 
validate=Validator.suffix_group(SuffixGroup.SAN_FIELDS)),
+    "inbound.conn.server-cert.san.": 
MapParams(target="INBOUND:SERVER-CERT:SAN", upper=True, prefix=True, 
validate=Validator.suffix_group(SuffixGroup.SAN_FIELDS)),
+    "inbound.conn.client-cert.": MapParams(target="INBOUND:CLIENT-CERT", 
upper=True, prefix=True, 
validate=Validator.suffix_group(SuffixGroup.CERT_FIELDS)),
+    "inbound.conn.server-cert.": MapParams(target="INBOUND:SERVER-CERT", 
upper=True, prefix=True, 
validate=Validator.suffix_group(SuffixGroup.CERT_FIELDS)),
+    "inbound.conn.": MapParams(target="INBOUND", upper=True, prefix=True, 
validate=Validator.suffix_group(SuffixGroup.CONN_FIELDS)),
+    "inbound.cookie.": MapParams(target="COOKIE", prefix=True, 
validate=Validator.http_token(), rev={"reverse_fallback": "inbound.cookie."}),
+    "inbound.req.": MapParams(target="CLIENT-HEADER", prefix=True, 
validate=Validator.http_header_name(), rev={"reverse_fallback": 
"inbound.req."}),
+    "inbound.resp.": MapParams(target="HEADER", prefix=True, 
validate=Validator.http_header_name(), rev={"reverse_context": 
"header_condition"}),
+    "inbound.url.": MapParams(target="CLIENT-URL", upper=True, prefix=True, 
validate=Validator.suffix_group(SuffixGroup.URL_FIELDS)),
+    "now.": MapParams(target="NOW", upper=True, 
validate=Validator.suffix_group(SuffixGroup.DATE_FIELDS)),
+    "outbound.conn.client-cert.SAN.": 
MapParams(target="OUTBOUND:CLIENT-CERT:SAN", upper=True, prefix=True, 
validate=Validator.suffix_group(SuffixGroup.SAN_FIELDS), 
sections={SectionType.PRE_REMAP, SectionType.REMAP, SectionType.READ_REQUEST}),
+    "outbound.conn.server-cert.SAN.": 
MapParams(target="OUTBOUND:SERVER-CERT:SAN", upper=True, prefix=True, 
validate=Validator.suffix_group(SuffixGroup.SAN_FIELDS), 
sections={SectionType.PRE_REMAP, SectionType.REMAP, SectionType.READ_REQUEST}),
+    "outbound.conn.client-cert.san.": 
MapParams(target="OUTBOUND:CLIENT-CERT:SAN", upper=True, prefix=True, 
validate=Validator.suffix_group(SuffixGroup.SAN_FIELDS), 
sections={SectionType.PRE_REMAP, SectionType.REMAP, SectionType.READ_REQUEST}),
+    "outbound.conn.server-cert.san.": 
MapParams(target="OUTBOUND:SERVER-CERT:SAN", upper=True, prefix=True, 
validate=Validator.suffix_group(SuffixGroup.SAN_FIELDS), 
sections={SectionType.PRE_REMAP, SectionType.REMAP, SectionType.READ_REQUEST}),
+    "outbound.conn.client-cert.": MapParams(target="OUTBOUND:CLIENT-CERT", 
upper=True, prefix=True, 
validate=Validator.suffix_group(SuffixGroup.CERT_FIELDS), 
sections={SectionType.PRE_REMAP, SectionType.REMAP, SectionType.READ_REQUEST}),
+    "outbound.conn.server-cert.": MapParams(target="OUTBOUND:SERVER-CERT", 
upper=True, prefix=True, 
validate=Validator.suffix_group(SuffixGroup.CERT_FIELDS), 
sections={SectionType.PRE_REMAP, SectionType.REMAP, SectionType.READ_REQUEST}),
+    "outbound.conn.": MapParams(target="OUTBOUND", upper=True, prefix=True, 
validate=Validator.suffix_group(SuffixGroup.CONN_FIELDS), 
sections={SectionType.PRE_REMAP, SectionType.REMAP, SectionType.READ_REQUEST}),
+    "outbound.cookie.": MapParams(target="COOKIE", prefix=True, 
validate=Validator.http_token(), sections={SectionType.PRE_REMAP, 
SectionType.REMAP, SectionType.READ_REQUEST}, rev={"reverse_fallback": 
"inbound.cookie."}),
+    "outbound.req.": MapParams(target="HEADER", prefix=True, 
validate=Validator.http_header_name(), sections={SectionType.PRE_REMAP, 
SectionType.REMAP, SectionType.READ_REQUEST}, rev={"reverse_context": 
"header_condition"}),
+    "outbound.resp.": MapParams(target="HEADER", prefix=True, 
validate=Validator.http_header_name(), sections={SectionType.PRE_REMAP, 
SectionType.REMAP, SectionType.READ_REQUEST, SectionType.SEND_REQUEST}, 
rev={"reverse_context": "header_condition"}),
+    "outbound.url.": MapParams(target="NEXT-HOP", upper=True, prefix=True, 
validate=Validator.suffix_group(SuffixGroup.URL_FIELDS), 
sections={SectionType.PRE_REMAP, SectionType.REMAP, SectionType.READ_REQUEST}),
+    "to.url.": MapParams(target="TO-URL", upper=True, prefix=True, 
validate=Validator.suffix_group(SuffixGroup.URL_FIELDS)),
 }
 
 FALLBACK_TAG_MAP: dict[str, tuple[str, bool]] = {
@@ -314,6 +142,7 @@ CONTEXT_TYPE_MAP: dict[str, str | tuple[str, str]] = {
 
 # Operator command mappings for reverse resolution
 OPERATOR_COMMAND_MAP: dict[str, tuple[str, str, Callable, Callable]] = {
+    "add-header": ("header_ops", "header", lambda toks: toks[1], lambda qual: 
qual),
     "set-header": ("header_ops", "header", lambda toks: toks[1], lambda qual: 
qual),
     "rm-header": ("header_ops", "header", lambda toks: toks[1], lambda qual: 
qual),
     "set-cookie": ("cookie_ops", "cookie", lambda toks: toks[1], lambda qual: 
qual),
@@ -321,6 +150,7 @@ OPERATOR_COMMAND_MAP: dict[str, tuple[str, str, Callable, 
Callable]] = {
     "set-destination": ("destination_ops", "destination", lambda toks: 
toks[1].lower(), lambda qual: qual),
     "rm-destination": ("destination_ops", "destination", lambda toks: 
toks[1].lower(), lambda qual: qual)
 }
+# yapf: enable
 
 REVERSE_RESOLUTION_MAP = get_complete_reverse_resolution_map()
 
@@ -412,23 +242,19 @@ class LSPPatternMatcher:
     @classmethod
     def match_any_pattern(cls, expression: str) -> PatternMatch | None:
         """Try to match expression against all pattern types."""
-        # Try field patterns first (most specific)
+
         if match := cls.match_field_pattern(expression):
             return match
 
-        # Try certificate patterns
         if match := cls.match_certificate_pattern(expression):
             return match
 
-        # Try connection patterns
         if match := cls.match_connection_pattern(expression):
             return match
 
-        # Try header patterns
         if match := cls.match_header_pattern(expression):
             return match
 
-        # Try cookie patterns
         if match := cls.match_cookie_pattern(expression):
             return match
 
diff --git a/tools/hrw4u/src/types.py b/tools/hrw4u/src/types.py
index 213e084a59..4185f78840 100644
--- a/tools/hrw4u/src/types.py
+++ b/tools/hrw4u/src/types.py
@@ -19,10 +19,14 @@ from __future__ import annotations
 
 from enum import Enum
 from dataclasses import dataclass
-from typing import Self
+from typing import Self, Callable, TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from hrw4u.states import SectionType
 
 
 class MagicStrings(str, Enum):
+    ADD_HEADER = "add-header"
     RM_HEADER = "rm-header"
     SET_HEADER = "set-header"
     RM_COOKIE = "rm-cookie"
@@ -166,3 +170,73 @@ class Symbol:
 
     def as_operator(self, value: str) -> str:
         return f"{self.var_type.op_tag} {self.index} {value}"
+
+
+class MapParams:
+    """Map parameters for table entries combining flags and metadata.
+    """
+
+    def __init__(
+            self,
+            upper: bool = False,
+            add: bool = False,
+            prefix: bool = False,
+            validate: Callable[[str], None] | None = None,
+            sections: set[SectionType] | None = None,
+            rev: dict | None = None,
+            target: str | list[str] | tuple[str, ...] | None = None) -> None:
+        object.__setattr__(
+            self, '_params', {
+                'upper': upper,
+                'add': add,
+                'prefix': prefix,
+                'validate': validate,
+                'sections': sections,
+                'rev': rev,
+                'target': target
+            })
+
+    def __getattr__(self, name: str):
+        if name.startswith('_'):
+            raise AttributeError(f"'{type(self).__name__}' object has no 
attribute '{name}'")
+        return self._params.get(name, False if name in ('upper', 'add', 
'prefix') else None)
+
+    def __setattr__(self, name: str, value: object) -> None:
+        """Prevent modification after initialization (immutable)."""
+        raise AttributeError(f"'{type(self).__name__}' object is immutable")
+
+    def __repr__(self) -> str:
+        non_defaults = []
+        for k, v in self._params.items():
+            if k in ('upper', 'add', 'prefix'):
+                if v:
+                    non_defaults.append(f"{k}=True")
+            elif v is not None:
+                if isinstance(v, set):
+                    non_defaults.append(f"{k}={{{', '.join(str(s) for s in 
v)}}}")
+                elif k == 'validate':
+                    non_defaults.append(f"{k}=<validator>")
+                else:
+                    non_defaults.append(f"{k}=...")
+
+        if not non_defaults:
+            return "MapParams()"
+        return f"MapParams({', '.join(non_defaults)})"
+
+    def __hash__(self) -> int:
+        hashable_items = []
+        for k, v in self._params.items():
+            if isinstance(v, set):
+                hashable_items.append((k, frozenset(v)))
+            elif isinstance(v, dict):
+                hashable_items.append((k, frozenset(v.items()) if v else None))
+            elif callable(v):
+                hashable_items.append((k, id(v)))
+            else:
+                hashable_items.append((k, v))
+        return hash(frozenset(hashable_items))
+
+    def __eq__(self, other: object) -> bool:
+        if not isinstance(other, MapParams):
+            return NotImplemented
+        return self._params == other._params
diff --git a/tools/hrw4u/src/visitor.py b/tools/hrw4u/src/visitor.py
index 5d35cb4ca6..c038cdc241 100644
--- a/tools/hrw4u/src/visitor.py
+++ b/tools/hrw4u/src/visitor.py
@@ -356,6 +356,18 @@ class HRW4UVisitor(hrw4uVisitor, BaseHRWVisitor):
                     self.emit_statement(out)
                     return
 
+                case _ if ctx.PLUSEQUAL():
+                    if ctx.lhs is None:
+                        raise SymbolResolutionError("assignment", "Missing 
left-hand side in += assignment")
+                    lhs = ctx.lhs.text
+                    rhs = ctx.value().getText()
+                    if rhs.startswith('"') and rhs.endswith('"'):
+                        rhs = self._substitute_strings(rhs, ctx)
+                    self._dbg(f"add assignment: {lhs} += {rhs}")
+                    out = self.symbol_resolver.resolve_add_assignment(lhs, 
rhs, self.current_section)
+                    self.emit_statement(out)
+                    return
+
                 case _:
                     if ctx.op is None:
                         raise SymbolResolutionError("operator", "Missing 
operator in statement")
diff --git a/tools/hrw4u/tests/data/hooks/remap.ast.txt 
b/tools/hrw4u/tests/data/hooks/remap.ast.txt
index 0b229e4e48..c8f2a6d632 100644
--- a/tools/hrw4u/tests/data/hooks/remap.ast.txt
+++ b/tools/hrw4u/tests/data/hooks/remap.ast.txt
@@ -1 +1 @@
-(program (programItem (section REMAP { (sectionBody (conditional (ifStatement 
if (condition (expression (term (factor (comparison (comparable 
inbound.req.X-Remap) == (value "yes")))))) (block { (blockItem (statement 
inbound.req.X-Remap = (value "") ;)) })) (elseClause else (block { (blockItem 
(statement inbound.req.X-Remap = (value "It was not yes") ;)) })))) })) <EOF>)
+(program (programItem (section REMAP { (sectionBody (conditional (ifStatement 
if (condition (expression (term (factor (comparison (comparable 
inbound.req.X-Remap) == (value "yes")))))) (block { (blockItem (statement 
inbound.req.X-Remap = (value "") ;)) (blockItem (statement 
inbound.req.X-Appended += (value "HRW4U") ;)) })) (elseClause else (block { 
(blockItem (statement inbound.req.X-Remap = (value "It was not yes") ;)) 
(blockItem (statement inbound.req.X-Appended = (value "") ;)) }))))  [...]
diff --git a/tools/hrw4u/tests/data/hooks/remap.input.txt 
b/tools/hrw4u/tests/data/hooks/remap.input.txt
index 0b31ed8c1c..1dd3c4d14b 100644
--- a/tools/hrw4u/tests/data/hooks/remap.input.txt
+++ b/tools/hrw4u/tests/data/hooks/remap.input.txt
@@ -1,7 +1,9 @@
 REMAP {
     if inbound.req.X-Remap == "yes" {
         inbound.req.X-Remap = "";
+        inbound.req.X-Appended += "HRW4U";
     } else {
         inbound.req.X-Remap = "It was not yes";
+        inbound.req.X-Appended = "";
     }
 }
diff --git a/tools/hrw4u/tests/data/hooks/remap.output.txt 
b/tools/hrw4u/tests/data/hooks/remap.output.txt
index 243fbb65c7..d151e91028 100644
--- a/tools/hrw4u/tests/data/hooks/remap.output.txt
+++ b/tools/hrw4u/tests/data/hooks/remap.output.txt
@@ -1,5 +1,7 @@
 cond %{REMAP_PSEUDO_HOOK} [AND]
 cond %{CLIENT-HEADER:X-Remap} ="yes"
     rm-header X-Remap
+    add-header X-Appended "HRW4U"
 else
     set-header X-Remap "It was not yes"
+    rm-header X-Appended
diff --git a/tools/hrw4u/tests/data/ops/exceptions.txt 
b/tools/hrw4u/tests/data/ops/exceptions.txt
index b96ebf7092..9628625f81 100644
--- a/tools/hrw4u/tests/data/ops/exceptions.txt
+++ b/tools/hrw4u/tests/data/ops/exceptions.txt
@@ -3,3 +3,5 @@
 
 # QSA (Query String Append) is a reverse-only test
 qsa.input: u4wrh
+# HTTP-CNTL valid bools can not reverse back to the original input
+http_cntl_valid_bools.input: hrw4u
diff --git a/tools/hrw4u/tests/data/ops/http_cntl_valid_bools.ast.txt 
b/tools/hrw4u/tests/data/ops/http_cntl_valid_bools.ast.txt
index 6029280f56..870d41622d 100644
--- a/tools/hrw4u/tests/data/ops/http_cntl_valid_bools.ast.txt
+++ b/tools/hrw4u/tests/data/ops/http_cntl_valid_bools.ast.txt
@@ -1 +1 @@
-(program (section SEND_RESPONSE { (sectionBody (statement http.cntl.LOGGING = 
(value TRUE) ;)) (sectionBody (statement http.cntl.TXN_DEBUG = (value FALSE) 
;)) (sectionBody (statement http.cntl.REQ_CACHEABLE = (value YES) ;)) 
(sectionBody (statement http.cntl.RESP_CACHEABLE = (value NO) ;)) (sectionBody 
(statement http.cntl.SERVER_NO_STORE = (value ON) ;)) (sectionBody (statement 
http.cntl.SKIP_REMAP = (value OFF) ;)) (sectionBody (statement 
http.cntl.INTERCEPT_RETRY = (value 1) ;)) (sect [...]
+(program (programItem (section SEND_RESPONSE { (sectionBody (statement 
http.cntl.LOGGING = (value TRUE) ;)) (sectionBody (statement 
http.cntl.TXN_DEBUG = (value FALSE) ;)) (sectionBody (statement 
http.cntl.REQ_CACHEABLE = (value YES) ;)) (sectionBody (statement 
http.cntl.RESP_CACHEABLE = (value NO) ;)) (sectionBody (statement 
http.cntl.SERVER_NO_STORE = (value ON) ;)) (sectionBody (statement 
http.cntl.SKIP_REMAP = (value OFF) ;)) (sectionBody (statement 
http.cntl.INTERCEPT_RETRY = (value [...]
diff --git a/tools/hrw4u/tests/data/ops/http_cntl_valid_bools.output.txt 
b/tools/hrw4u/tests/data/ops/http_cntl_valid_bools.output.txt
index d90fac00d6..9fbb2661e2 100644
--- a/tools/hrw4u/tests/data/ops/http_cntl_valid_bools.output.txt
+++ b/tools/hrw4u/tests/data/ops/http_cntl_valid_bools.output.txt
@@ -15,4 +15,4 @@ cond %{SEND_RESPONSE_HDR_HOOK} [AND]
     set-http-cntl INTERCEPT_RETRY off
     set-http-cntl LOGGING True
     set-http-cntl TXN_DEBUG False
-    set-http-cntl LOGGING TRue
\ No newline at end of file
+    set-http-cntl LOGGING TRue
diff --git a/tools/hrw4u/tests/data/ops/qsa.output.txt 
b/tools/hrw4u/tests/data/ops/qsa.output.txt
index 6ea3587798..03e002907a 100644
--- a/tools/hrw4u/tests/data/ops/qsa.output.txt
+++ b/tools/hrw4u/tests/data/ops/qsa.output.txt
@@ -2,4 +2,4 @@
 # test, because in hrw4u, we don't use QSA.
 cond %{REMAP_PSEUDO_HOOK} [AND]
 cond %{GEO:COUNTRY} =SE
-    set-redirect 302 https://www.example.com/SE/%{CLIENT-URL:PATH} [QSA]
+    set-redirect 302 
"https://www.example.com/SE/%{CLIENT-URL:PATH}?%{CLIENT-URL:QUERY}";

Reply via email to