Repository: corazawaf/coraza
Pull request #538 (2022-12-22 07:14:58)

doc: v3 changelog
This is an initial version of a version 3 changelog for coraza.

sts: @corazawaf/core-developers Please review the texts, especially your own commits. Some of the api changes posed bit of a headache to me, to work something out. I think the goal would be to give enough comment to introduce the changes and give a bit of a reasoning why they should be used.
jcchavezs: Thanks @sts for owning this <3.
jptosso: I think we are almost ready to release, we should update this PR with the latest data, I think we have been making more changes in the transaction interface

Excelent work @sts , thank you very much! 

Patch:
diff --git a/.github/workflows/tinygo.yml b/.github/workflows/tinygo.yml
index 35bc5df..14f95d9 100644
--- a/.github/workflows/tinygo.yml
+++ b/.github/workflows/tinygo.yml
@@ -20,12 +20,9 @@ jobs:
   test:
     strategy:
       matrix:
-        # TODO(anuraaga): Add 1.19.x after TinyGo adds support for it.
-        go-version: [1.18.x]
+        go-version: [1.19.x]
         os: [ubuntu-latest]
     runs-on: ${{ matrix.os }}
-    container:
-      image: tinygo/tinygo:0.24.0
     steps:
       - name: Checkout code
         uses: actions/checkout@v3
@@ -36,6 +33,11 @@ jobs:
           go-version: ${{ matrix.go-version }}
           cache: true
 
+      - name: setup tinygo
+        uses: acifani/setup-tinygo@v1
+        with:
+          tinygo-version: 0.26.0
+
       - name: Cache TinyGo build
         uses: actions/cache@v3
         with:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8f7e29b..18b6eab 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,128 @@
 
 * Decided for Golang semantic versioning [#208](https://github.com/corazawaf/coraza/issues/208)
 
+* BREAKING: Required Go version gets bumped to 1.18 [#343](https://github.com/corazawaf/coraza/pull/343)
+
+* **CRS v4 Support** - [#218](https://github.com/corazawaf/coraza/issues/218) -
+Coraza v3 is fully compatible with the OWASP Core Rule Set v4. Support for Core Rule Set 3.x is dropped as rules are still based on PCRE. CRS v4 is solely based on RE2 and will soon see a public & official release.  
+
+  CRS further moved away from its dependency on old Regexp::Assemble to the new crs-toolchain helper to prevent rules incompatible with Coraza.
+
+* **FEATURE: TinyGo & WASM support & Proxy-WASM Connector** - [#254](https://github.com/corazawaf/coraza/pull/254) -
+Coraza adds initial support for TinyGo to allow compiling to Web Assembly (WASM). Compatible directives are marked with *Tinygo Compatibility* in the documentation.
+
+  Based on contributions sponsored by the Tetrate.io team, a new [coraza-proxy-wasm connector](https://github.com/corazawaf/coraza-proxy-wasm) is developed which can be loaded in [Envoy Proxy](https://www.envoyproxy.io) or as Istio plugin.
+
+  Special thanks to the Tetrate team, Anuraag Agrawal, Matteo Pace and José Carlos Chávez. This greatly enhances our coverage, as now we support Envoy proxy and any WASM-proxy compatible system.
+
+* **FEATURE: SecDatasets & operators** - [#361](https://github.com/corazawaf/coraza/pull/361) -
+SecDatasets are added as replacement for .data files. WASM support is an essential feature of Coraza v3, but users cannot fully enjoy its potential because of file reading limitations. For this reason, SecDataset is a decent replacement for .data files.
+
+  Two new SecLang operators are added which can be used to query datasets. `pmFromDataset`  [#361](https://github.com/corazawaf/coraza/pull/361) and `ipMatchFromDataset` [#75e8217](https://github.com/corazawaf/coraza/commit/75e821700de9fbfafde6c763f474c7add8dab319) which can be used instead of their file based equivalents for those environments which can't access the filesystem.
+
+
+```apache
+SecDataset restricted-files-1 `
+.my.cnf
+.mysql_history
+`
+SecRule REQUEST_FILENAME "@pmFromDataset restricted-files-1" \
+    "...msg:'Match sample_dataset'"
+```
+
+* **FEATURE: Restpath Operator** - [#282](https://github.com/corazawaf/coraza/pull/282) - `@restpath` takes a path pattern as a parameter eg. `/path/to/{id}/{name}` and aids evaluation of application urls. The path will be transformed to a regex and assigned to variables in `ARGS_PATH`, `ARGS_NAMES`, and `ARGS`. 
+
+```apache
+SecRule REQUEST_URI "@restpath /some/random/url/{id}/{name}" "…..chain"
+SecRule ARGS_PATH:id "!@eq %{user:session_id}" "deny"
+```
+
+* **FEATURE: Redirect Operator** - [#290](https://github.com/corazawaf/coraza/pull/290) -
+Redirect takes a status code and url as parameter and based on this information returns an http redirect to the client. 
+
+```apache
+SecRule REQUEST_URI "/redirect"  "phase:1,id:1,status:302,redirect:http://www.example.com
+```
+
+* **Enhanced Multipart Support** - [#452](https://github.com/corazawaf/coraza/pull/452) -
+Pick up the MULTIPART_PART_HEADERS support found in ModSecurity [reference](https://github.com/SpiderLabs/ModSecurity/wiki/Reference-Manual-(v2.x)-Variables#MULTIPART_PART_HEADERS).
+
+> This variable is a collection of all part headers found within the request body with Content-Type multipart/form-data. The key of each item in the collection is the name of the part in which it was found, while the value is the entire part-header line -- including both the part-header name and the part-header value.
+
+```apache
+SecRule ARGS:_msg_body "@rx Hi Martin," "id:200, phase:2,log"
+SecRule MULTIPART_PART_HEADERS:_msg_body "Content-Disposition" "id:250, phase:2, log"
+SecRule MULTIPART_PART_HEADERS "Content-Disposition" "id:300, phase:2, log"
+```
+
+### Performance Optimizations
+
+Similar performance to modsecurity is archived, we are faster or slower, depending on the payload. Usually, big payloads work better in Coraza. Coraza v3 is ~50% faster than Coraza v2.0.1, more than 200% faster than Coraza v2.0.0.
+
+- Replace the Aho Corasick string matching implementation used internally with Petar Dambovaliev’s implementation (60% less memory consumption and 233% faster execution time) [#302](https://github.com/corazawaf/coraza/pull/302)
+- Optimize validateNID operator [#30e5b56](https://github.com/corazawaf/coraza/commit/30e5b564d4d7c6688fb819c97b0891e097570a2e) [#348](https://github.com/corazawaf/coraza/pull/348)
+- Use io.Discard instead of /dev/null to save a syscall when debug log output should be discarded [#354](https://github.com/corazawaf/coraza/pull/354)
+- Optimize Body Buffering [#505](https://github.com/corazawaf/coraza/pull/505)
+- Remove unused mutex in RuleGroup [#381](https://github.com/corazawaf/coraza/pull/381)
+- Speed up random string generation by switching to a pseudorandom generator [#403](https://github.com/corazawaf/coraza/pull/403)
+- Improve SecLang parser performance [#412](https://github.com/corazawaf/coraza/pull/412) 
+- Use strings.Builder to avoid copy string input to bytes in `urlencode` [#320]((https://github.com/corazawaf/coraza/pull/320) and `base64decode` [#319](https://github.com/corazawaf/coraza/pull/319) 
+- Use lookup table for byte range validation in `validateByteRange` [#490](https://github.com/corazawaf/coraza/pull/490)
+
+### API Changes
+
+A lot of effort was added to optimize and clean up the Coraza API which resulted in a couple of breaking API changes.
+
+- **New Variables Engine** - [#277](https://github.com/corazawaf/coraza/pull/277) - Implements a new Variables Engine similar to modsecurity. Variables have two pointers `tx.Collections[]` and `tx.variables.*` which either allow programmatic access using the proper collection mechanism or using dynamic variable names.
+
+  There are multiple variable types (Simple, Map, Proxy, Translation) with different helpers and generic helpers. Each type has its own variable (string, map, proxy, etc.)  https://github.com/corazawaf/coraza/tree/v3/dev/collection 
+
+  **BREAKING**: Raw data can only be accessed through a RequestBodyProcessor
+
+- **BREAKING**: Library entry points are converted to immutable interfaces [#397](https://github.com/corazawaf/coraza/pull/397)
+
+  This simplifies the interface and provides a safe mechanism to invoke Coraza and handle transactions. It further allows for major changes without updating the public API and maintains compatibility. Any calls to seclang.NewParser can be removed.
+
+  The approach for doing this is to first move existing interfaces into internal/corazawaf, internal/seclang and start creating public API from scratch delegating to these. After completing migration, there is probably cleanup that could be done which may result in the removal of delegation, but that could happen after locking in a public API.
+
+
+
+- **BREAKING**: Rename Waf to WAF to follow Go type naming conventions of acronyms. `coraza.NewWaf` must be accessed as `coraza.NewWAF` and `tx.Waf` as `tx.WAF`. [#373](https://github.com/corazawaf/coraza/pull/373)
+
+- **BREAKING**: Rename methods of *Transaction* [#518](https://github.com/corazawaf/coraza/pull/518)
+  - Interrupted &rarr; IsInterrupted
+  - ResponseBodyAccessible &rarr; IsResponseBodyAccessible
+  - IsProcessableResponseBody &rarr; IsResponseBodyProcessable
+  - RequestBodyAccessible &rarr; IsRequestBodyAccessible
+
+- **BREAKING: Remove ZAP** - [#682b59](https://github.com/corazawaf/coraza/commit/6828b59811f5a1b0b86213533a71ec9aaea229c8) [#b64ede7](https://github.com/corazawaf/coraza/commit/b64ede757c7409d7ab9e441bbdfcf6157a3aa6b0) - Debug logging is now an interface and Zap is removed . Support to allow `SecDebugLog` to log to `/dev/stderr` or `/dev/stdout` is added in [#449](https://github.com/corazawaf/coraza/pull/#449)
+
+- **BREAKING: WAFConfig Type**: A new immutable WAFConfig type is added to initialize Coraza, which replaces seclang.NewParser.
+Each WithXXX function (WithRules, WithDirectives, WithDirectivesFromFile, WithAuditLog, WithContentInjection, WithRequestBodyAccess, WithResponseBodyAccess, WithDebugLogger, WithErrorLogger, WithRootFS) of this type returns a new instance including the corresponding change. [#bffb435/config.go#L1
+
+- **Rules, Action & Transaction Interfaces** - New immutable interfaces are added
+
+- **BREAKING**: Convert rulematch types to interfaces [#478](https://github.com/corazawaf/coraza/pull/478)
+
+- **BREAKING**: https://github.com/corazawaf/coraza/pull/503
+
+- **BREAKING**: Add RequestBodyAccessible() and ResponseBodyAccessible() to types.transaction.
+
+- **BREAKING**: Export Request/Response BodyAccess values [#499](https://github.com/corazawaf/coraza/pull/)
+
+
+### Testing
+
+- Introduce new CRS testing suite for Coraza v3 based on Go HTTPServer and go-ftw. Remove Caddy to avoid circular project dependency [#457](https://github.com/corazawaf/coraza/pull/457)
+- Automatically perform Benchmarks to detect performance regressions [#301](https://github.com/corazawaf/coraza/pull/301)
+- Enhance the test engine to perform tests of returned interruptions (output.interruption).
+- Add testing for disruptive actions.
+- Switch to mage instead of pre-commit [#315](https://github.com/corazawaf/coraza/pull/315) [#355](https://github.com/corazawaf/coraza/pull/355) [#356](https://github.com/corazawaf/coraza/pull/356)
+
+
+
+--------------------------------------------------------------
+
 ## Coraza v2  (March 22, 2022)
 
 BUG FIXES:
diff --git a/actions/ctl.go b/actions/ctl.go
index 7cfceca..55847fc 100644
--- a/actions/ctl.go
+++ b/actions/ctl.go
@@ -168,7 +168,7 @@ func (a *ctlFn) Evaluate(r rules.RuleMetadata, txS rules.TransactionState) {
 	case ctlHashEnforcement:
 		// Not supported yet
 	case ctlDebugLogLevel:
-		// lvl, _ := strconv.Atoi(a.Value)
+		// lvl, _ := strconv.Atoi(a.value)
 		// TODO
 		// We cannot update the log level, it would affect the whole waf instance...
 		// tx.WAF.SetLogLevel(lvl)
diff --git a/actions/setvar.go b/actions/setvar.go
index 5f68e51..816b027 100644
--- a/actions/setvar.go
+++ b/actions/setvar.go
@@ -68,7 +68,7 @@ func (a *setvarFn) Type() rules.ActionType {
 }
 
 func (a *setvarFn) evaluateTxCollection(r rules.RuleMetadata, tx rules.TransactionState, key string, value string) {
-	col := (tx.Collection(a.collection)).(*collection.Map)
+	col := (tx.Collection(a.collection)).(collection.Map)
 	if col == nil {
 		// fmt.Println("Invalid Collection " + a.Collection) LOG error?
 		return
diff --git a/bodyprocessors/json.go b/bodyprocessors/json.go
index ef9f2cd..d8cce85 100644
--- a/bodyprocessors/json.go
+++ b/bodyprocessors/json.go
@@ -25,11 +25,7 @@ func (js *jsonBodyProcessor) ProcessRequest(reader io.Reader, v rules.Transactio
 	argsGetCol := v.ArgsGet()
 	for key, value := range data {
 		// TODO: This hack prevent GET variables from overriding POST variables
-		for k := range argsGetCol.Data() {
-			if k == key {
-				argsGetCol.Remove(k)
-			}
-		}
+		argsGetCol.Remove(key)
 		col.SetIndex(key, 0, value)
 	}
 	return nil
diff --git a/bodyprocessors/xml.go b/bodyprocessors/xml.go
index c407894..18bafb8 100644
--- a/bodyprocessors/xml.go
+++ b/bodyprocessors/xml.go
@@ -36,6 +36,9 @@ func readXML(reader io.Reader) ([]string, []string, error) {
 	var attrs []string
 	var content []string
 	dec := xml.NewDecoder(reader)
+	dec.Strict = false
+	dec.AutoClose = xml.HTMLAutoClose
+	dec.Entity = xml.HTMLEntity
 	for {
 		token, err := dec.Token()
 		if err != nil && err != io.EOF {
diff --git a/collection/collection.go b/collection/collection.go
index ce094a6..37d077f 100644
--- a/collection/collection.go
+++ b/collection/collection.go
@@ -14,18 +14,59 @@ import (
 // to store slices of data for keys
 // Important: CollectionMaps ARE NOT concurrent safe
 type Collection interface {
+	// FindAll returns matches for all the items in this Collection.
+	FindAll() []types.MatchData
+
+	// Name returns the name for the current CollectionMap
+	Name() string
+
+	// Reset the current Collection
+	Reset()
+}
+
+// Single is a Collection with a single element.
+type Single interface {
+	Collection
+
+	// Get returns the value of this Single
+	Get() string
+
+	// Set sets the value of this Single
+	Set(string)
+}
+
+// Keyed is a Collection with elements that can be selected by key.
+type Keyed interface {
+	Collection
+
+	// Get returns a slice of strings for a key
+	Get(key string) []string
+
 	// FindRegex returns a slice of MatchData for the regex
 	FindRegex(key *regexp.Regexp) []types.MatchData
 
 	// FindString returns a slice of MatchData for the string
 	FindString(key string) []types.MatchData
+}
 
-	// FindString returns a slice of MatchData for the string
-	FindAll() []types.MatchData
+// Map are used to store VARIABLE data
+// for transactions, this data structured is designed
+// to store slices of data for keys
+// Important: CollectionMaps ARE NOT concurrent safe
+type Map interface {
+	Keyed
 
-	// Name returns the name for the current CollectionMap
-	Name() string
+	// Add a value to some key
+	Add(key string, value string)
 
-	// Reset the current CollectionMap
-	Reset()
+	// Set will replace the key's value with this slice
+	Set(key string, values []string)
+
+	// SetIndex will place the value under the index
+	// If the index is higher than the current size of the CollectionMap
+	// it will be appended
+	SetIndex(key string, index int, value string)
+
+	// Remove deletes the key from the CollectionMap
+	Remove(key string)
 }
diff --git a/collection/collection_map.go b/collection/collection_map.go
deleted file mode 100644
index 4319335..0000000
--- a/collection/collection_map.go
+++ /dev/null
@@ -1,214 +0,0 @@
-// Copyright 2022 Juan Pablo Tosso and the OWASP Coraza contributors
-// SPDX-License-Identifier: Apache-2.0
-
-package collection
-
-import (
-	"regexp"
-
-	"github.com/corazawaf/coraza/v3/internal/corazarules"
-	"github.com/corazawaf/coraza/v3/types"
-	"github.com/corazawaf/coraza/v3/types/variables"
-)
-
-// Map are used to store VARIABLE data
-// for transactions, this data structured is designed
-// to store slices of data for keys
-// Important: CollectionMaps ARE NOT concurrent safe
-type Map struct {
-	data     map[string][]types.AnchoredVar
-	name     string
-	variable variables.RuleVariable
-}
-
-// Get returns a slice of strings for a key
-func (c *Map) Get(key string) []string {
-	var values []string
-	for _, a := range c.data[key] {
-		values = append(values, a.Value)
-	}
-	return values
-}
-
-// FindRegex returns a slice of MatchData for the regex
-func (c *Map) FindRegex(key *regexp.Regexp) []types.MatchData {
-	var result []types.MatchData
-	for k, data := range c.data {
-		if key.MatchString(k) {
-			for _, d := range data {
-				result = append(result, &corazarules.MatchData{
-					VariableName_: c.name,
-					Variable_:     c.variable,
-					Key_:          d.Name,
-					Value_:        d.Value,
-				})
-			}
-		}
-	}
-	return result
-}
-
-// FindString returns a slice of MatchData for the string
-func (c *Map) FindString(key string) []types.MatchData {
-	var result []types.MatchData
-	if key == "" {
-		return c.FindAll()
-	}
-	// if key is not empty
-	if e, ok := c.data[key]; ok {
-		for _, aVar := range e {
-			result = append(result, &corazarules.MatchData{
-				VariableName_: c.name,
-				Variable_:     c.variable,
-				Key_:          aVar.Name,
-				Value_:        aVar.Value,
-			})
-		}
-	}
-	return result
-}
-
-// FindAll returns all the contained elements
-func (c *Map) FindAll() []types.MatchData {
-	var result []types.MatchData
-	for _, data := range c.data {
-		for _, d := range data {
-			result = append(result, &corazarules.MatchData{
-				VariableName_: c.name,
-				Variable_:     c.variable,
-				Key_:          d.Name,
-				Value_:        d.Value,
-			})
-		}
-	}
-	return result
-}
-
-func (c *Map) keysRx(rx *regexp.Regexp) []string {
-	var keys []string
-	for k := range c.data {
-		if rx.MatchString(k) {
-			keys = append(keys, k)
-		}
-	}
-	return keys
-}
-
-func (c *Map) keys() []string {
-	var keys []string
-	for k := range c.data {
-		keys = append(keys, k)
-	}
-	return keys
-}
-
-// AddCS a value to some key with case sensitive vKey
-func (c *Map) AddCS(key string, vKey string, vVal string) {
-	aVal := types.AnchoredVar{Name: vKey, Value: vVal}
-	c.data[key] = append(c.data[key], aVal)
-}
-
-// Add a value to some key
-func (c *Map) Add(key string, value string) {
-	c.AddCS(key, key, value)
-}
-
-// AddUniqueCS will add a value to a key if it is not already there
-// with case sensitive vKey
-func (c *Map) AddUniqueCS(key string, vKey string, vVal string) {
-	if c.data[key] == nil {
-		c.AddCS(key, vKey, vVal)
-		return
-	}
-
-	for _, a := range c.data[key] {
-		if a.Value == vVal {
-			return
-		}
-	}
-	c.AddCS(key, vKey, vVal)
-}
-
-// AddUnique will add a value to a key if it is not already there
-func (c *Map) AddUnique(key string, value string) {
-	c.AddUniqueCS(key, key, value)
-}
-
-// SetCS will replace the key's value with this slice
-// internally converts [] string to []types.AnchoredVar
-// with case sensitive vKey
-func (c *Map) SetCS(key string, vKey string, values []string) {
-	c.data[key] = make([]types.AnchoredVar, 0, len(values))
-	for _, v := range values {
-		c.data[key] = append(c.data[key], types.AnchoredVar{Name: vKey, Value: v})
-	}
-}
-
-// Set will replace the key's value with this slice
-// internally converts [] string to []types.AnchoredVar
-func (c *Map) Set(key string, values []string) {
-	c.SetCS(key, key, values)
-}
-
-// SetIndexCS will place the value under the index
-// If the index is higher than the current size of the CollectionMap
-// it will be appended
-// with case sensitive vKey
-func (c *Map) SetIndexCS(key string, index int, vKey string, value string) {
-	if c.data[key] == nil {
-		c.data[key] = []types.AnchoredVar{{Name: vKey, Value: value}}
-	}
-	vVal := types.AnchoredVar{Name: vKey, Value: value}
-	if len(c.data[key]) <= index {
-		c.data[key] = append(c.data[key], vVal)
-		return
-	}
-	c.data[key][index] = vVal
-}
-
-// SetIndex will place the value under the index
-// If the index is higher than the current size of the CollectionMap
-// it will be appended
-func (c *Map) SetIndex(key string, index int, value string) {
-	c.SetIndexCS(key, index, key, value)
-}
-
-// Remove deletes the key from the CollectionMap
-func (c *Map) Remove(key string) {
-	delete(c.data, key)
-}
-
-// Name returns the name for the current CollectionMap
-func (c *Map) Name() string {
-	return c.name
-}
-
-// Reset the current CollectionMap
-func (c *Map) Reset() {
-	for k := range c.data {
-		delete(c.data, k)
-	}
-}
-
-// Data returns all the data in the CollectionMap
-func (c *Map) Data() map[string][]string {
-	result := map[string][]string{}
-	for k, v := range c.data {
-		result[k] = make([]string, 0, len(v))
-		for _, a := range v {
-			result[k] = append(result[k], a.Value)
-		}
-	}
-	return result
-}
-
-var _ Collection = &Map{}
-
-// NewMap returns a collection of key->[]values
-func NewMap(variable variables.RuleVariable) *Map {
-	return &Map{
-		name:     variable.Name(),
-		variable: variable,
-		data:     map[string][]types.AnchoredVar{},
-	}
-}
diff --git a/collection/collection_proxy.go b/collection/collection_proxy.go
deleted file mode 100644
index 078bf20..0000000
--- a/collection/collection_proxy.go
+++ /dev/null
@@ -1,88 +0,0 @@
-// Copyright 2022 Juan Pablo Tosso and the OWASP Coraza contributors
-// SPDX-License-Identifier: Apache-2.0
-
-package collection
-
-import (
-	"regexp"
-
-	"github.com/corazawaf/coraza/v3/types"
-	"github.com/corazawaf/coraza/v3/types/variables"
-)
-
-// Proxy are used to store VARIABLE data
-// for transactions, this data structured is designed
-// to store slices of data for keys
-// Important: CollectionProxys ARE NOT concurrent safe
-type Proxy struct {
-	data     []*Map
-	name     string
-	variable variables.RuleVariable
-}
-
-// FindRegex returns a slice of MatchData for the regex
-func (c *Proxy) FindRegex(key *regexp.Regexp) []types.MatchData {
-	var res []types.MatchData
-	for _, c := range c.data {
-		res = append(res, c.FindRegex(key)...)
-	}
-	return res
-}
-
-// FindString returns a slice of MatchData for the string
-func (c *Proxy) FindString(key string) []types.MatchData {
-	var res []types.MatchData
-	for _, c := range c.data {
-		res = append(res, c.FindString(key)...)
-	}
-	return res
-}
-
-// FindAll returns all matches for all collections
-func (c *Proxy) FindAll() []types.MatchData {
-	var res []types.MatchData
-	for _, c := range c.data {
-		res = append(res, c.FindAll()...)
-	}
-	return res
-}
-
-// Name returns the name for the current CollectionProxy
-func (c *Proxy) Name() string {
-	return c.name
-}
-
-// Get returns the data for the key
-func (c *Proxy) Get(key string) []string {
-	var res []string
-	for _, c := range c.data {
-		res = append(res, c.Get(key)...)
-	}
-	return res
-}
-
-// Data returns merged data from all CollectionMap
-func (c *Proxy) Data() map[string][]string {
-	res := map[string][]string{}
-	for _, c := range c.data {
-		for k, v := range c.Data() {
-			res[k] = append(res[k], v...)
-		}
-	}
-	return res
-}
-
-// Reset the current CollectionProxy
-func (c *Proxy) Reset() {
-}
-
-var _ Collection = &Proxy{}
-
-// NewProxy returns a Proxy collection that merges all collections
-func NewProxy(variable variables.RuleVariable, data ...*Map) *Proxy {
-	return &Proxy{
-		name:     variable.Name(),
-		variable: variable,
-		data:     data,
-	}
-}
diff --git a/collection/collection_simple.go b/collection/collection_simple.go
deleted file mode 100644
index a7044f9..0000000
--- a/collection/collection_simple.go
+++ /dev/null
@@ -1,86 +0,0 @@
-// Copyright 2022 Juan Pablo Tosso and the OWASP Coraza contributors
-// SPDX-License-Identifier: Apache-2.0
-
-package collection
-
-import (
-	"regexp"
-	"strconv"
-
-	"github.com/corazawaf/coraza/v3/internal/corazarules"
-	"github.com/corazawaf/coraza/v3/types"
-	"github.com/corazawaf/coraza/v3/types/variables"
-)
-
-// Simple are used to store VARIABLE data
-// for transactions, this data structured is designed
-// to store slices of data for keys
-// Important: CollectionSimples ARE NOT concurrent safe
-type Simple struct {
-	data     string
-	name     string
-	variable variables.RuleVariable
-}
-
-// FindRegex returns a slice of MatchData for the regex
-func (c *Simple) FindRegex(key *regexp.Regexp) []types.MatchData {
-	return c.FindAll()
-}
-
-// FindString returns a slice of MatchData for the string
-func (c *Simple) FindString(key string) []types.MatchData {
-	return c.FindAll()
-}
-
-// FindAll returns a single MatchData for the current data
-func (c *Simple) FindAll() []types.MatchData {
-	return []types.MatchData{
-		&corazarules.MatchData{
-			VariableName_: c.name,
-			Variable_:     c.variable,
-			Value_:        c.data,
-		},
-	}
-}
-
-// String returns the first string occurrence of a key
-func (c *Simple) String() string {
-	return c.data
-}
-
-// Int64 returns the first int64 occurrence of a key
-func (c *Simple) Int64() int64 {
-	return int64(c.Int())
-}
-
-// Int returns the first int occurrence of a key
-func (c *Simple) Int() int {
-	r, _ := strconv.ParseInt(c.data, 10, 32)
-	return int(r)
-}
-
-// Set will replace the key's value with this slice
-// internally converts [] string to []types.AnchoredVar
-func (c *Simple) Set(value string) {
-	c.data = value
-}
-
-// Name returns the name for the current CollectionSimple
-func (c *Simple) Name() string {
-	return c.name
-}
-
-// Reset the current CollectionSimple
-func (c *Simple) Reset() {
-	c.data = ""
-}
-
-var _ Collection = &Simple{}
-
-// NewSimple creates a new CollectionSimple
-func NewSimple(variable variables.RuleVariable) *Simple {
-	return &Simple{
-		variable: variable,
-		name:     variable.Name(),
-	}
-}
diff --git a/collection/collection_translation_proxy.go b/collection/collection_translation_proxy.go
deleted file mode 100644
index 2f0485c..0000000
--- a/collection/collection_translation_proxy.go
+++ /dev/null
@@ -1,109 +0,0 @@
-// Copyright 2022 Juan Pablo Tosso and the OWASP Coraza contributors
-// SPDX-License-Identifier: Apache-2.0
-
-package collection
-
-import (
-	"regexp"
-
-	"github.com/corazawaf/coraza/v3/internal/corazarules"
-	"github.com/corazawaf/coraza/v3/types"
-	"github.com/corazawaf/coraza/v3/types/variables"
-)
-
-// TranslationProxy are used to store VARIABLE data
-// for transactions, this data structured is designed
-// to store slices of data for keys
-// Important: CollectionTranslationProxys ARE NOT concurrent safe
-type TranslationProxy struct {
-	data     []*Map
-	name     string
-	variable variables.RuleVariable
-}
-
-// FindRegex returns a slice of MatchData for the regex
-func (c *TranslationProxy) FindRegex(key *regexp.Regexp) []types.MatchData {
-	var res []types.MatchData
-	for _, c := range c.data {
-		for _, k := range c.keysRx(key) {
-			res = append(res, &corazarules.MatchData{
-				VariableName_: c.name,
-				Variable_:     c.variable,
-				Value_:        k,
-			})
-		}
-	}
-	return res
-}
-
-// FindString returns a slice of MatchData for the string
-func (c *TranslationProxy) FindString(key string) []types.MatchData {
-	for _, c := range c.data {
-		if len(c.Get(key)) > 0 {
-			return []types.MatchData{
-				&corazarules.MatchData{
-					VariableName_: c.name,
-					Variable_:     c.variable,
-					Value_:        key,
-				},
-			}
-		}
-	}
-	return nil
-}
-
-// FindAll returns all keys from Proxy Collections
-func (c *TranslationProxy) FindAll() []types.MatchData {
-	var res []types.MatchData
-	for _, c := range c.data {
-		for _, k := range c.keys() {
-			res = append(res, &corazarules.MatchData{
-				VariableName_: c.name,
-				Variable_:     c.variable,
-				Value_:        k,
-			})
-		}
-	}
-	return res
-}
-
-// Data returns the keys of all Proxy collections
-func (c *TranslationProxy) Data() []string {
-	var res []string
-	for _, c := range c.data {
-		res = append(res, c.keys()...)
-	}
-	return res
-}
-
-// Name returns the name for the current CollectionTranslationProxy
-func (c *TranslationProxy) Name() string {
-	return c.name
-}
-
-// Reset the current CollectionTranslationProxy
-func (c *TranslationProxy) Reset() {
-	// do nothing
-}
-
-// Get the value for the index
-func (c *TranslationProxy) Get(index int) string {
-	if index < len(c.data) {
-		if v := c.data[index].Get(""); len(v) > 0 {
-			return v[0]
-		}
-	}
-	return ""
-}
-
-var _ Collection = &TranslationProxy{}
-
-// NewTranslationProxy creates a translation proxy
-// Translation proxies are used to merge variable keys from multiple collections
-func NewTranslationProxy(variable variables.RuleVariable, data ...*Map) *TranslationProxy {
-	return &TranslationProxy{
-		name:     variable.Name(),
-		variable: variable,
-		data:     data,
-	}
-}
diff --git a/config.go b/config.go
index 9495da3..04d5e86 100644
--- a/config.go
+++ b/config.go
@@ -70,15 +70,9 @@ type WAFConfig interface {
 	WithRootFS(fs fs.FS) WAFConfig
 }
 
-const unsetLimit = -1
-
 // NewWAFConfig creates a new WAFConfig with the default settings.
 func NewWAFConfig() WAFConfig {
-	return &wafConfig{
-		requestBodyLimit:         unsetLimit,
-		requestBodyInMemoryLimit: unsetLimit,
-		responseBodyLimit:        unsetLimit,
-	}
+	return &wafConfig{}
 }
 
 // AuditLogConfig controls audit logging.
@@ -111,10 +105,10 @@ type wafConfig struct {
 	rules                    []wafRule
 	auditLog                 *auditLogConfig
 	requestBodyAccess        bool
-	requestBodyLimit         int
-	requestBodyInMemoryLimit int
+	requestBodyLimit         *int
+	requestBodyInMemoryLimit *int
 	responseBodyAccess       bool
-	responseBodyLimit        int
+	responseBodyLimit        *int
 	responseBodyMimeTypes    []string
 	debugLogger              loggers.DebugLogger
 	errorCallback            func(rule types.MatchedRule)
@@ -191,19 +185,19 @@ func (c *wafConfig) clone() *wafConfig {
 
 func (c *wafConfig) WithRequestBodyLimit(limit int) WAFConfig {
 	ret := c.clone()
-	ret.requestBodyLimit = limit
+	ret.requestBodyLimit = &limit
 	return ret
 }
 
 func (c *wafConfig) WithRequestBodyInMemoryLimit(limit int) WAFConfig {
 	ret := c.clone()
-	ret.requestBodyInMemoryLimit = limit
+	ret.requestBodyInMemoryLimit = &limit
 	return ret
 }
 
 func (c *wafConfig) WithResponseBodyLimit(limit int) WAFConfig {
 	ret := c.clone()
-	ret.responseBodyLimit = limit
+	ret.responseBodyLimit = &limit
 	return ret
 }
 
diff --git a/coraza.conf-recommended b/coraza.conf-recommended
index 128332c..b5166d4 100644
--- a/coraza.conf-recommended
+++ b/coraza.conf-recommended
@@ -42,6 +42,9 @@ SecRule REQUEST_HEADERS:Content-Type "^application/json" \
 # low as practical.
 #
 SecRequestBodyLimit 13107200
+
+SecRequestBodyInMemoryLimit 131072
+
 SecRequestBodyNoFilesLimit 131072
 
 # What to do if the request body size is above our configured limit.
@@ -205,12 +208,12 @@ SecDataDir /tmp/
 
 # Default debug log path
 # Debug levels:
-# 1: fatal
-# 2: panic
-# 3: error
-# 4: warn
-# 5: info
-# 6: debug
+# 0:   No logging (least verbose)
+# 1:   Error
+# 2:   Warn
+# 3:   Info
+# 4-8: Debug
+# 9:   Trace (most verbose)
 # Most logging has not been implemented because it will be replaced with
 # advanced rule profiling options
 #SecDebugLog /opt/coraza/var/log/debug.log
diff --git a/examples/http-server/default.conf b/examples/http-server/default.conf
index 9b4c6b5..7f3edc5 100644
--- a/examples/http-server/default.conf
+++ b/examples/http-server/default.conf
@@ -1,4 +1,4 @@
-SecDebugLogLevel 5
+SecDebugLogLevel 9
 SecDebugLog /dev/stdout
 
 SecRule ARGS:id "@eq 0" "id:1, phase:1,deny, status:403,msg:'Invalid id',log,auditlog"
diff --git a/http/interceptor.go b/http/interceptor.go
index 7165d38..92047d3 100644
--- a/http/interceptor.go
+++ b/http/interceptor.go
@@ -10,6 +10,7 @@ package http
 import (
 	"fmt"
 	"io"
+	"log"
 	"net/http"
 
 	"github.com/corazawaf/coraza/v3/types"
@@ -18,19 +19,19 @@ import (
 // rwInterceptor intercepts the ResponseWriter, so it can track response size
 // and returned status code.
 type rwInterceptor struct {
-	w             http.ResponseWriter
-	tx            types.Transaction
-	statusCode    int
-	proto         string
-	hasStatusCode bool
+	w                  http.ResponseWriter
+	tx                 types.Transaction
+	statusCode         int
+	proto              string
+	isWriteHeaderFlush bool
+	wroteHeader        bool
 }
 
+// WriteHeader records the status code to be sent right before the moment
+// the body is being written.
 func (i *rwInterceptor) WriteHeader(statusCode int) {
-	if i.hasStatusCode {
-		if i.tx.IsInterrupted() {
-			// If we already set a status code, but phase4 raised an interruption, let's change it
-			i.statusCode = statusCode
-		}
+	if i.wroteHeader {
+		log.Println("http: superfluous response.WriteHeader call")
 		return
 	}
 
@@ -40,33 +41,65 @@ func (i *rwInterceptor) WriteHeader(statusCode int) {
 		}
 	}
 
-	i.hasStatusCode = true
 	i.statusCode = statusCode
 	if it := i.tx.ProcessResponseHeaders(statusCode, i.proto); it != nil {
 		i.statusCode = obtainStatusCodeFromInterruptionOrDefault(it, i.statusCode)
+		i.flushWriteHeader()
+		return
 	}
+
+	i.wroteHeader = true
 }
 
-func (i *rwInterceptor) Write(b []byte) (int, error) {
-	if !i.hasStatusCode {
-		i.WriteHeader(http.StatusOK)
+// overrideWriteHeader overrides the recorded status code
+func (i *rwInterceptor) overrideWriteHeader(statusCode int) {
+	i.statusCode = statusCode
+}
+
+// flushWriteHeader sends the status code to the delegate writers
+func (i *rwInterceptor) flushWriteHeader() {
+	if !i.isWriteHeaderFlush {
+		i.w.WriteHeader(i.statusCode)
+		i.isWriteHeaderFlush = true
 	}
+}
+
+// Write buffers the response body until the request body limit is reach or an
+// interruption is triggered, this buffer is later used to analyse the body in
+// the response processor.
+// If the body isn't accessible or the mime type isn't processable, the response
+// body is being writen to the delegate response writer directly.
+func (i *rwInterceptor) Write(b []byte) (int, error) {
 	if i.tx.IsInterrupted() {
-		// if there is an interruption it must be from phase 4 and hence
-		// we won't write anything to either the body or the buffer.
+		// if there is an interruption it must be from at least phase 4 and hence
+		// WriteHeader or Write should have been called and hence the status code
+		// has been flushed to the delegated response writer.
 		return 0, nil
 	}
 
-	if i.tx.IsResponseBodyAccessible() {
+	if !i.wroteHeader {
+		// if no header has been wrote at this point we aim to return 200
+		i.WriteHeader(http.StatusOK)
+	}
+
+	if i.tx.IsResponseBodyAccessible() && i.tx.IsResponseBodyProcessable() {
 		// we only buffer the response body if we are going to access
 		// to it, otherwise we just send it to the response writer.
 		it, n, err := i.tx.WriteResponseBody(b)
 		if it != nil {
-			i.WriteHeader(http.StatusForbidden)
+			i.overrideWriteHeader(it.Status)
+			// We only flush the status code after an interruption.
+			i.flushWriteHeader()
 			return 0, nil
 		}
 		return n, err
 	}
+
+	// flush the status code before writing
+	i.flushWriteHeader()
+
+	// if response body isn't accesible or processable we write the response bytes
+	// directly to the caller.
 	return i.w.Write(b)
 }
 
@@ -87,43 +120,44 @@ func wrap(w http.ResponseWriter, r *http.Request, tx types.Transaction) (
 	func(types.Transaction, *http.Request) error,
 ) { // nolint:gocyclo
 
-	i := &rwInterceptor{w: w, tx: tx, proto: r.Proto}
+	i := &rwInterceptor{w: w, tx: tx, proto: r.Proto, statusCode: 200}
 
 	responseProcessor := func(tx types.Transaction, r *http.Request) error {
-		// We look for interruptions determined at phase 4 (response headers)
-		// as body hasn't being analized yet.
+		// We look for interruptions triggered at phase 4 (response headers)
+		// and during writing the response body. If so, response status code
+		// has been sent over the flush already.
 		if tx.IsInterrupted() {
-			// phase 4 interruption stops execution
-			i.w.WriteHeader(i.statusCode)
 			return nil
 		}
 
 		if tx.IsResponseBodyAccessible() && tx.IsResponseBodyProcessable() {
 			if it, err := tx.ProcessResponseBody(); err != nil {
-				i.w.WriteHeader(http.StatusInternalServerError)
+				i.overrideWriteHeader(http.StatusInternalServerError)
+				i.flushWriteHeader()
 				return err
 			} else if it != nil {
-				i.w.WriteHeader(obtainStatusCodeFromInterruptionOrDefault(it, i.statusCode))
+				i.overrideWriteHeader(obtainStatusCodeFromInterruptionOrDefault(it, i.statusCode))
+				i.flushWriteHeader()
 				return nil
 			}
 
 			// we release the buffer
 			reader, err := tx.ResponseBodyReader()
 			if err != nil {
-				i.w.WriteHeader(http.StatusInternalServerError)
+				i.overrideWriteHeader(http.StatusInternalServerError)
+				i.flushWriteHeader()
 				return fmt.Errorf("failed to release the response body reader: %v", err)
 			}
 
 			// this is the last opportunity we have to report the resolved status code
 			// as next step is write into the response writer (triggering a 200 in the
 			// response status code.)
-			i.w.WriteHeader(i.statusCode)
+			i.flushWriteHeader()
 			if _, err := io.Copy(w, reader); err != nil {
-				i.w.WriteHeader(http.StatusInternalServerError)
 				return fmt.Errorf("failed to copy the response body: %v", err)
 			}
 		} else {
-			i.w.WriteHeader(i.statusCode)
+			i.flushWriteHeader()
 		}
 
 		return nil
diff --git a/http/middleware.go b/http/middleware.go
index 16345ae..85d897b 100644
--- a/http/middleware.go
+++ b/http/middleware.go
@@ -44,9 +44,13 @@ func processRequest(tx types.Transaction, req *http.Request) (*types.Interruptio
 			tx.AddRequestHeader(k, v)
 		}
 	}
-	// Host will always be removed from req.Headers(), so we manually add it
+
+	// Host will always be removed from req.Headers() and promoted to the
+	// Request.Host field, so we manually add it
 	if req.Host != "" {
 		tx.AddRequestHeader("Host", req.Host)
+		// This connector relies on the host header (now host field) to populate ServerName
+		tx.SetServerName(req.Host)
 	}
 
 	in = tx.ProcessRequestHeaders()
@@ -103,6 +107,11 @@ func processRequest(tx types.Transaction, req *http.Request) (*types.Interruptio
 }
 
 func WrapHandler(waf coraza.WAF, l Logger, h http.Handler) http.Handler {
+	if waf == nil {
+		l("nil WAF passed to the handler wrapper")
+		return h
+	}
+
 	fn := func(w http.ResponseWriter, r *http.Request) {
 		tx := waf.NewTransaction()
 		defer func() {
diff --git a/internal/collections/concat.go b/internal/collections/concat.go
new file mode 100644
index 0000000..ff6f459
--- /dev/null
+++ b/internal/collections/concat.go
@@ -0,0 +1,110 @@
+// Copyright 2023 Juan Pablo Tosso and the OWASP Coraza contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package collections
+
+import (
+	"regexp"
+	"strings"
+
+	"github.com/corazawaf/coraza/v3/collection"
+	"github.com/corazawaf/coraza/v3/types"
+	"github.com/corazawaf/coraza/v3/types/variables"
+)
+
+// ConcatCollection is a collection view over multiple sollections.
+type ConcatCollection struct {
+	data     []collection.Collection
+	name     string
+	variable variables.RuleVariable
+}
+
+var _ collection.Collection = &ConcatCollection{}
+
+func NewConcatCollection(variable variables.RuleVariable, data ...collection.Collection) *ConcatCollection {
+	return &ConcatCollection{
+		data:     data,
+		name:     variable.Name(),
+		variable: variable,
+	}
+}
+
+// FindAll returns all matches for all collections
+func (c *ConcatCollection) FindAll() []types.MatchData {
+	var res []types.MatchData
+	for _, c := range c.data {
+		res = append(res, c.FindAll()...)
+	}
+	return res
+}
+
+// Name returns the name for the current CollectionconcatCollection
+func (c *ConcatCollection) Name() string {
+	return c.name
+}
+
+// Reset the current ConcatMap
+func (c *ConcatCollection) Reset() {
+}
+
+// ConcatKeyed is a collection view over multiple keyed collections.
+type ConcatKeyed struct {
+	data     []collection.Keyed
+	name     string
+	variable variables.RuleVariable
+}
+
+var _ collection.Keyed = &ConcatKeyed{}
+
+func NewConcatKeyed(variable variables.RuleVariable, data ...collection.Keyed) *ConcatKeyed {
+	return &ConcatKeyed{
+		data:     data,
+		name:     variable.Name(),
+		variable: variable,
+	}
+}
+
+func (c *ConcatKeyed) Get(key string) []string {
+	keyL := strings.ToLower(key)
+	var res []string
+	for _, c := range c.data {
+		res = append(res, c.Get(keyL)...)
+	}
+	return res
+}
+
+// FindRegex returns a slice of MatchData for the regex
+func (c *ConcatKeyed) FindRegex(key *regexp.Regexp) []types.MatchData {
+	var res []types.MatchData
+	for _, c := range c.data {
+		res = append(res, c.FindRegex(key)...)
+	}
+	return res
+}
+
+// FindString returns a slice of MatchData for the string
+func (c *ConcatKeyed) FindString(key string) []types.MatchData {
+	var res []types.MatchData
+	for _, c := range c.data {
+		res = append(res, c.FindString(key)...)
+	}
+	return res
+}
+
+// FindAll returns all matches for all collections
+func (c *ConcatKeyed) FindAll() []types.MatchData {
+	var res []types.MatchData
+	for _, c := range c.data {
+		res = append(res, c.FindAll()...)
+	}
+	return res
+}
+
+// Name returns the name for the current CollectionconcatCollection
+func (c *ConcatKeyed) Name() string {
+	return c.name
+}
+
+// Reset the current ConcatMap
+func (c *ConcatKeyed) Reset() {
+}
diff --git a/internal/collections/map.go b/internal/collections/map.go
new file mode 100644
index 0000000..a55c34a
--- /dev/null
+++ b/internal/collections/map.go
@@ -0,0 +1,171 @@
+// Copyright 2023 Juan Pablo Tosso and the OWASP Coraza contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package collections
+
+import (
+	"regexp"
+	"strings"
+
+	"github.com/corazawaf/coraza/v3/collection"
+	"github.com/corazawaf/coraza/v3/internal/corazarules"
+	"github.com/corazawaf/coraza/v3/types"
+	"github.com/corazawaf/coraza/v3/types/variables"
+)
+
+// Map is a default collection.Map.
+type Map struct {
+	data     map[string][]keyValue
+	name     string
+	variable variables.RuleVariable
+}
+
+var _ collection.Map = &Map{}
+
+func NewMap(variable variables.RuleVariable) *Map {
+	return &Map{
+		name:     variable.Name(),
+		variable: variable,
+		data:     map[string][]keyValue{},
+	}
+}
+
+func (c *Map) Get(key string) []string {
+	if len(c.data) == 0 {
+		return nil
+	}
+	keyL := strings.ToLower(key)
+	var values []string
+	for _, a := range c.data[keyL] {
+		values = append(values, a.value)
+	}
+	return values
+}
+
+func (c *Map) FindRegex(key *regexp.Regexp) []types.MatchData {
+	var result []types.MatchData
+	for k, data := range c.data {
+		if key.MatchString(k) {
+			for _, d := range data {
+				result = append(result, &corazarules.MatchData{
+					VariableName_: c.name,
+					Variable_:     c.variable,
+					Key_:          d.key,
+					Value_:        d.value,
+				})
+			}
+		}
+	}
+	return result
+}
+
+func (c *Map) FindString(key string) []types.MatchData {
+	var result []types.MatchData
+	if key == "" {
+		return c.FindAll()
+	}
+	if len(c.data) == 0 {
+		return nil
+	}
+	keyL := strings.ToLower(key)
+	// if key is not empty
+	if e, ok := c.data[keyL]; ok {
+		for _, aVar := range e {
+			result = append(result, &corazarules.MatchData{
+				VariableName_: c.name,
+				Variable_:     c.variable,
+				Key_:          aVar.key,
+				Value_:        aVar.value,
+			})
+		}
+	}
+	return result
+}
+
+func (c *Map) FindAll() []types.MatchData {
+	var result []types.MatchData
+	for _, data := range c.data {
+		for _, d := range data {
+			result = append(result, &corazarules.MatchData{
+				VariableName_: c.name,
+				Variable_:     c.variable,
+				Key_:          d.key,
+				Value_:        d.value,
+			})
+		}
+	}
+	return result
+}
+
+func (c *Map) Add(key string, value string) {
+	keyL := strings.ToLower(key)
+	aVal := keyValue{key: key, value: value}
+	c.data[keyL] = append(c.data[keyL], aVal)
+}
+
+func (c *Map) Set(key string, values []string) {
+	keyL := strings.ToLower(key)
+	c.data[keyL] = make([]keyValue, 0, len(values))
+	for _, v := range values {
+		c.data[keyL] = append(c.data[keyL], keyValue{key: key, value: v})
+	}
+}
+
+func (c *Map) SetIndex(key string, index int, value string) {
+	keyL := strings.ToLower(key)
+	values := c.data[keyL]
+	av := keyValue{key: key, value: value}
+
+	switch {
+	case len(values) == 0:
+		c.data[keyL] = []keyValue{av}
+	case len(values) <= index:
+		c.data[keyL] = append(c.data[keyL], av)
+	default:
+		c.data[keyL][index] = av
+	}
+}
+
+func (c *Map) Remove(key string) {
+	if len(c.data) == 0 {
+		return
+	}
+	keyL := strings.ToLower(key)
+	delete(c.data, keyL)
+}
+
+func (c *Map) Name() string {
+	return c.name
+}
+
+func (c *Map) Reset() {
+	for k := range c.data {
+		delete(c.data, k)
+	}
+}
+
+func (c *Map) String() string {
+	res := strings.Builder{}
+	res.WriteString(c.name)
+	res.WriteString(":\n")
+	for k, v := range c.data {
+		res.WriteString("    ")
+		res.WriteString(k)
+		res.WriteString(": ")
+		for i, vv := range v {
+			if i > 0 {
+				res.WriteString(",")
+			}
+			res.WriteString(vv.value)
+		}
+		res.WriteByte('\n')
+	}
+	return res.String()
+}
+
+// keyValue stores the case preserved original key and value
+// of the variable
+type keyValue struct {
+	key   string
+	value string
+}
diff --git a/internal/collections/named.go b/internal/collections/named.go
new file mode 100644
index 0000000..bad5501
--- /dev/null
+++ b/internal/collections/named.go
@@ -0,0 +1,149 @@
+// Copyright 2023 Juan Pablo Tosso and the OWASP Coraza contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package collections
+
+import (
+	"fmt"
+	"regexp"
+	"strings"
+
+	"github.com/corazawaf/coraza/v3/collection"
+	"github.com/corazawaf/coraza/v3/internal/corazarules"
+	"github.com/corazawaf/coraza/v3/types"
+	"github.com/corazawaf/coraza/v3/types/variables"
+)
+
+// NamedCollection is a Collection that also keeps track of unique names.
+type NamedCollection struct {
+	*Map
+	names []string
+}
+
+var _ collection.Map = &NamedCollection{}
+
+func NewNamedCollection(rv variables.RuleVariable) *NamedCollection {
+	return &NamedCollection{
+		Map: NewMap(rv),
+	}
+}
+
+// Add a value to some key
+func (c *NamedCollection) Add(key string, value string) {
+	c.Map.Add(key, value)
+	c.addName(key)
+}
+
+// Set will replace the key's value with this slice
+func (c *NamedCollection) Set(key string, values []string) {
+	c.Map.Set(key, values)
+	c.addName(key)
+}
+
+// SetIndex will place the value under the index
+// If the index is higher than the current size of the CollectionMap
+// it will be appended
+func (c *NamedCollection) SetIndex(key string, index int, value string) {
+	c.Map.SetIndex(key, index, value)
+	c.addName(key)
+}
+
+// Remove deletes the key from the CollectionMap
+func (c *NamedCollection) Remove(key string) {
+	c.Map.Remove(key)
+	for i, n := range c.names {
+		if n == key {
+			c.names = append(c.names[:i], c.names[i+1:]...)
+			return
+		}
+	}
+}
+
+// Data is an internal method used for serializing to JSON
+func (c *NamedCollection) Data() map[string][]string {
+	result := map[string][]string{}
+	for k, v := range c.data {
+		result[k] = make([]string, 0, len(v))
+		for _, a := range v {
+			result[k] = append(result[k], a.value)
+		}
+	}
+	return result
+}
+
+// Name returns the name for the current CollectionMap
+func (c *NamedCollection) Name() string {
+	return c.Map.Name()
+}
+
+func (c *NamedCollection) Reset() {
+	c.Map.Reset()
+	c.names = nil
+}
+
+func (c *NamedCollection) Names(rv variables.RuleVariable) collection.Collection {
+	return &NamedCollectionNames{
+		name:       rv.Name(),
+		variable:   rv,
+		collection: c,
+	}
+}
+
+func (c *NamedCollection) String() string {
+	return fmt.Sprint(c.Map)
+}
+
+func (c *NamedCollection) addName(key string) {
+	for _, n := range c.names {
+		if n == key {
+			return
+		}
+	}
+	c.names = append(c.names, key)
+}
+
+type NamedCollectionNames struct {
+	name       string
+	variable   variables.RuleVariable
+	collection *NamedCollection
+}
+
+func (c *NamedCollectionNames) FindRegex(key *regexp.Regexp) []types.MatchData {
+	panic("selection operator not supported")
+}
+
+func (c *NamedCollectionNames) FindString(key string) []types.MatchData {
+	panic("selection operator not supported")
+}
+
+func (c *NamedCollectionNames) FindAll() []types.MatchData {
+	var res []types.MatchData
+	for _, k := range c.collection.names {
+		res = append(res, &corazarules.MatchData{
+			VariableName_: c.name,
+			Variable_:     c.variable,
+			Value_:        k,
+		})
+	}
+	return res
+}
+
+func (c *NamedCollectionNames) Name() string {
+	return c.name
+}
+
+func (c *NamedCollectionNames) Reset() {
+}
+
+func (c *NamedCollectionNames) String() string {
+	res := strings.Builder{}
+	res.WriteString(c.name)
+	res.WriteString(": ")
+	for i, k := range c.collection.names {
+		if i > 0 {
+			res.WriteString(",")
+		}
+		res.WriteString(k)
+	}
+	return res.String()
+}
diff --git a/internal/collections/single.go b/internal/collections/single.go
new file mode 100644
index 0000000..4848afb
--- /dev/null
+++ b/internal/collections/single.go
@@ -0,0 +1,59 @@
+// Copyright 2023 Juan Pablo Tosso and the OWASP Coraza contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package collections
+
+import (
+	"fmt"
+
+	"github.com/corazawaf/coraza/v3/collection"
+	"github.com/corazawaf/coraza/v3/internal/corazarules"
+	"github.com/corazawaf/coraza/v3/types"
+	"github.com/corazawaf/coraza/v3/types/variables"
+)
+
+type Single struct {
+	data     string
+	name     string
+	variable variables.RuleVariable
+}
+
+var _ collection.Single = &Single{}
+
+// NewSingle creates a new Single.
+func NewSingle(variable variables.RuleVariable) *Single {
+	return &Single{
+		variable: variable,
+		name:     variable.Name(),
+	}
+}
+
+func (c *Single) FindAll() []types.MatchData {
+	return []types.MatchData{
+		&corazarules.MatchData{
+			VariableName_: c.name,
+			Variable_:     c.variable,
+			Value_:        c.data,
+		},
+	}
+}
+
+func (c *Single) Get() string {
+	return c.data
+}
+
+func (c *Single) Set(value string) {
+	c.data = value
+}
+
+func (c *Single) Name() string {
+	return c.name
+}
+
+func (c *Single) Reset() {
+	c.data = ""
+}
+
+func (c *Single) String() string {
+	return fmt.Sprintf("%s: %s", c.name, c.data)
+}
diff --git a/collection/collection_size_proxy.go b/internal/collections/sized.go
similarity index 54%
rename from collection/collection_size_proxy.go
rename to internal/collections/sized.go
index 3cb4128..5de5386 100644
--- a/collection/collection_size_proxy.go
+++ b/internal/collections/sized.go
@@ -1,78 +1,82 @@
-// Copyright 2022 Juan Pablo Tosso and the OWASP Coraza contributors
+// Copyright 2023 Juan Pablo Tosso and the OWASP Coraza contributors
 // SPDX-License-Identifier: Apache-2.0
 
-package collection
+package collections
 
 import (
+	"fmt"
 	"regexp"
 	"strconv"
 
+	"github.com/corazawaf/coraza/v3/collection"
 	"github.com/corazawaf/coraza/v3/internal/corazarules"
 	"github.com/corazawaf/coraza/v3/types"
 	"github.com/corazawaf/coraza/v3/types/variables"
 )
 
-// SizeProxy are used to connect the size
-// of many collection map values and return the sum
-type SizeProxy struct {
-	data     []*Map
+type SizeCollection struct {
+	data     []*NamedCollection
 	name     string
 	variable variables.RuleVariable
 }
 
+var _ collection.Collection = &SizeCollection{}
+
+// NewSizeCollection returns a collection that
+// only returns the total sum of all the collections values
+func NewSizeCollection(variable variables.RuleVariable, data ...*NamedCollection) *SizeCollection {
+	return &SizeCollection{
+		name:     variable.Name(),
+		variable: variable,
+		data:     data,
+	}
+}
+
 // FindRegex returns a slice of MatchData for the regex
-func (c *SizeProxy) FindRegex(*regexp.Regexp) []types.MatchData {
+func (c *SizeCollection) FindRegex(*regexp.Regexp) []types.MatchData {
 	return c.FindAll()
 }
 
 // FindString returns a slice of MatchData for the string
-func (c *SizeProxy) FindString(string) []types.MatchData {
+func (c *SizeCollection) FindString(string) []types.MatchData {
 	return c.FindAll()
 }
 
 // FindAll returns a slice of MatchData of all matches
-func (c *SizeProxy) FindAll() []types.MatchData {
+func (c *SizeCollection) FindAll() []types.MatchData {
 	return []types.MatchData{
 		&corazarules.MatchData{
 			VariableName_: c.name,
 			Variable_:     c.variable,
-			Value_:        strconv.FormatInt(c.Size(), 10),
+			Value_:        strconv.Itoa(c.size()),
 		},
 	}
 }
 
-// Size returns the size of all the collections values
-func (c *SizeProxy) Size() int64 {
-	i := 0
-	for _, d := range c.data {
-		// we iterate over d
-		for _, data := range d.data {
-			for _, v := range data {
-				i += len(v.Value)
-			}
-		}
-	}
-	return int64(i)
-}
-
 // Name returns the name for the current CollectionSizeProxy
-func (c *SizeProxy) Name() string {
+func (c *SizeCollection) Name() string {
 	return c.name
 }
 
 // Reset the current CollectionSizeProxy
-func (c *SizeProxy) Reset() {
+func (c *SizeCollection) Reset() {
 	// do nothing
 }
 
-var _ Collection = &SizeProxy{}
+func (c *SizeCollection) String() string {
+	return fmt.Sprintf("%s: %d", c.name, c.size())
+}
 
-// NewCollectionSizeProxy returns a collection that
-// only returns the total sum of all the collections values
-func NewCollectionSizeProxy(variable variables.RuleVariable, data ...*Map) *SizeProxy {
-	return &SizeProxy{
-		name:     variable.Name(),
-		variable: variable,
-		data:     data,
+// Size returns the size of all the collections values
+func (c *SizeCollection) size() int {
+	i := 0
+	for _, d := range c.data {
+		// we iterate over d
+		for _, data := range d.data {
+			for _, v := range data {
+				i += len(v.value)
+			}
+		}
 	}
+	return i
 }
diff --git a/internal/corazawaf/rule.go b/internal/corazawaf/rule.go
index 31df73f..db9b8ef 100644
--- a/internal/corazawaf/rule.go
+++ b/internal/corazawaf/rule.go
@@ -143,9 +143,6 @@ type Rule struct {
 	// If true, the transformations will be multi matched
 	MultiMatch bool
 
-	// Used for error logging
-	Disruptive bool
-
 	HasChain bool
 
 	// inferredPhases is the inferred phases the rule is relevant for
@@ -164,8 +161,8 @@ func (r *Rule) Status() int {
 // Evaluate will evaluate the current rule for the indicated transaction
 // If the operator matches, actions will be evaluated, and it will return
 // the matched variables, keys and values (MatchData)
-func (r *Rule) Evaluate(phase types.RulePhase, tx rules.TransactionState, cache map[transformationKey]*transformationValue) []types.MatchData {
-	return r.doEvaluate(phase, tx.(*Transaction), cache)
+func (r *Rule) Evaluate(phase types.RulePhase, tx rules.TransactionState, cache map[transformationKey]*transformationValue) {
+	r.doEvaluate(phase, tx.(*Transaction), cache)
 }
 
 func (r *Rule) doEvaluate(phase types.RulePhase, tx *Transaction, cache map[transformationKey]*transformationValue) []types.MatchData {
@@ -279,7 +276,7 @@ func (r *Rule) doEvaluate(phase types.RulePhase, tx *Transaction, cache map[tran
 		// we only run the chains for the parent rule
 		for nr := r.Chain; nr != nil; {
 			tx.WAF.Logger.Debug("[%s] [%d] Evaluating rule chain for %d", tx.id, rid, r.ID_)
-			matchedChainValues := nr.Evaluate(phase, tx, cache)
+			matchedChainValues := nr.doEvaluate(phase, tx, cache)
 			if len(matchedChainValues) == 0 {
 				return matchedChainValues
 			}
diff --git a/internal/corazawaf/transaction.go b/internal/corazawaf/transaction.go
index b0b5ddd..e0180b3 100644
--- a/internal/corazawaf/transaction.go
+++ b/internal/corazawaf/transaction.go
@@ -18,6 +18,7 @@ import (
 
 	"github.com/corazawaf/coraza/v3/bodyprocessors"
 	"github.com/corazawaf/coraza/v3/collection"
+	"github.com/corazawaf/coraza/v3/internal/collections"
 	"github.com/corazawaf/coraza/v3/internal/corazarules"
 	stringsutil "github.com/corazawaf/coraza/v3/internal/strings"
 	urlutil "github.com/corazawaf/coraza/v3/internal/url"
@@ -337,24 +338,22 @@ func (tx *Transaction) AddRequestHeader(key string, value string) {
 		return
 	}
 	keyl := strings.ToLower(key)
-	tx.variables.requestHeadersNames.AddUniqueCS(keyl, key, keyl)
-	tx.variables.requestHeaders.AddCS(keyl, key, value)
+	tx.variables.requestHeaders.Add(key, value)
 
-	if keyl == "content-type" {
+	switch keyl {
+	case "content-type":
 		val := strings.ToLower(value)
 		if val == "application/x-www-form-urlencoded" {
 			tx.variables.reqbodyProcessor.Set("URLENCODED")
 		} else if strings.HasPrefix(val, "multipart/form-data") {
 			tx.variables.reqbodyProcessor.Set("MULTIPART")
 		}
-	} else if keyl == "cookie" {
+	case "cookie":
 		// Cookies use the same syntax as GET params but with semicolon (;) separator
 		values := urlutil.ParseQuery(value, ';')
 		for k, vr := range values {
-			kl := strings.ToLower(k)
-			tx.variables.requestCookiesNames.AddUniqueCS(kl, k, kl)
 			for _, v := range vr {
-				tx.variables.requestCookies.AddCS(kl, k, v)
+				tx.variables.requestCookies.Add(k, v)
 			}
 		}
 	}
@@ -368,8 +367,7 @@ func (tx *Transaction) AddResponseHeader(key string, value string) {
 		return
 	}
 	keyl := strings.ToLower(key)
-	tx.variables.responseHeadersNames.AddUniqueCS(keyl, key, keyl)
-	tx.variables.responseHeaders.AddCS(keyl, key, value)
+	tx.variables.responseHeaders.Add(key, value)
 
 	// Most headers can be managed like that
 	if keyl == "content-type" {
@@ -471,30 +469,21 @@ func (tx *Transaction) ParseRequestReader(data io.Reader) (*types.Interruption,
 // matchVariable Creates the MATCHED_ variables required by chains and macro expansion
 // MATCHED_VARS, MATCHED_VAR, MATCHED_VAR_NAME, MATCHED_VARS_NAMES
 func (tx *Transaction) matchVariable(match *corazarules.MatchData) {
-	var varName, varNamel string
+	var varName string
 	if match.Key_ != "" {
 		varName = match.VariableName_ + ":" + match.Key_
-		varNamel = match.VariableName_ + ":" + strings.ToLower(match.Key_)
 	} else {
 		varName = match.VariableName_
-		varNamel = match.VariableName_
 	}
 	// Array of values
 	matchedVars := tx.variables.matchedVars
 	// Last key
 	matchedVarName := tx.variables.matchedVarName
 	matchedVarName.Reset()
-	// Array of keys
-	matchedVarsNames := tx.variables.matchedVarsNames
 
-	// We add the key in lowercase for ease of lookup in chains
-	// This is similar to args handling
-	matchedVars.AddCS(varNamel, varName, match.Value_)
+	matchedVars.Add(varName, match.Value_)
 	tx.variables.matchedVar.Set(match.Value_)
 
-	// We add the key in lowercase for ease of lookup in chains
-	// This is similar to args handling
-	matchedVarsNames.AddCS(varNamel, varName, varName)
 	matchedVarName.Set(varName)
 }
 
@@ -510,16 +499,16 @@ func (tx *Transaction) MatchRule(r *Rule, mds []types.MatchData) {
 
 	// set highest_severity
 	hs := tx.variables.highestSeverity
-	maxSeverity, _ := types.ParseRuleSeverity(hs.String())
+	maxSeverity, _ := types.ParseRuleSeverity(hs.Get())
 	if r.Severity_ > maxSeverity {
 		hs.Set(strconv.Itoa(r.Severity_.Int()))
 	}
 
 	mr := &corazarules.MatchedRule{
-		URI_:             tx.variables.requestURI.String(),
+		URI_:             tx.variables.requestURI.Get(),
 		TransactionID_:   tx.id,
-		ServerIPAddress_: tx.variables.serverAddr.String(),
-		ClientIPAddress_: tx.variables.remoteAddr.String(),
+		ServerIPAddress_: tx.variables.serverAddr.Get(),
+		ClientIPAddress_: tx.variables.remoteAddr.Get(),
 		Rule_:            &r.RuleMetadata,
 		MatchedDatas_:    mds,
 	}
@@ -557,7 +546,6 @@ func (tx *Transaction) GetStopWatch() string {
 // In future releases we may remove de exceptions slice and
 // make it easier to use
 func (tx *Transaction) GetField(rv ruleVariableParams) []types.MatchData {
-	collection := rv.Variable
 	col := tx.Collection(rv.Variable)
 	if col == nil {
 		return []types.MatchData{}
@@ -565,14 +553,21 @@ func (tx *Transaction) GetField(rv ruleVariableParams) []types.MatchData {
 
 	var matches []types.MatchData
 	// Now that we have access to the collection, we can apply the exceptions
-	if rv.KeyRx == nil {
-		if len(rv.KeyStr) == 0 {
-			matches = col.FindAll()
+	switch {
+	case rv.KeyRx != nil:
+		if m, ok := col.(collection.Keyed); ok {
+			matches = m.FindRegex(rv.KeyRx)
 		} else {
-			matches = col.FindString(rv.KeyStr)
+			panic("attempted to use regex with non-selectable collection: " + rv.Variable.Name())
 		}
-	} else {
-		matches = col.FindRegex(rv.KeyRx)
+	case rv.KeyStr != "":
+		if m, ok := col.(collection.Keyed); ok {
+			matches = m.FindString(rv.KeyStr)
+		} else {
+			panic("attempted to use string with non-selectable collection: " + rv.Variable.Name())
+		}
+	default:
+		matches = col.FindAll()
 	}
 
 	var rmi []int
@@ -602,8 +597,8 @@ func (tx *Transaction) GetField(rv ruleVariableParams) []types.MatchData {
 		count := len(matches)
 		matches = []types.MatchData{
 			&corazarules.MatchData{
-				VariableName_: collection.Name(),
-				Variable_:     collection,
+				VariableName_: rv.Variable.Name(),
+				Variable_:     rv.Variable,
 				Key_:          rv.KeyStr,
 				Value_:        strconv.Itoa(count),
 			},
@@ -631,7 +626,6 @@ func (tx *Transaction) RemoveRuleByID(id int) {
 // ProcessConnection should be called at very beginning of a request process, it is
 // expected to be executed prior to the virtual host resolution, when the
 // connection arrives on the server.
-// Important: Remember to check for a possible intervention.
 func (tx *Transaction) ProcessConnection(client string, cPort int, server string, sPort int) {
 	p := strconv.Itoa(cPort)
 	p2 := strconv.Itoa(sPort)
@@ -666,7 +660,7 @@ func (tx *Transaction) ExtractArguments(orig types.ArgumentType, uri string) {
 // ARGS_(GET|POST)_NAMES
 func (tx *Transaction) AddArgument(argType types.ArgumentType, key string, value string) {
 	// TODO implement ARGS value limit using ArgumentsLimit
-	var vals *collection.Map
+	var vals collection.Map
 	switch argType {
 	case types.ArgumentGET:
 		vals = tx.variables.argsGet
@@ -677,9 +671,8 @@ func (tx *Transaction) AddArgument(argType types.ArgumentType, key string, value
 	default:
 		return
 	}
-	keyl := strings.ToLower(key)
 
-	vals.AddCS(keyl, key, value)
+	vals.Add(key, value)
 }
 
 // ProcessURI Performs the analysis on the URI and all the query string variables.
@@ -742,6 +735,17 @@ func (tx *Transaction) ProcessURI(uri string, method string, httpVersion string)
 	tx.variables.queryString.Set(query)
 }
 
+// SetServerName allows to set server name details.
+//
+// The API consumer is in charge of retrieving the value (e.g. from the host header).
+// It is expected to be executed before calling ProcessRequestHeaders.
+func (tx *Transaction) SetServerName(serverName string) {
+	if tx.LastPhase >= types.PhaseRequestHeaders {
+		tx.WAF.Logger.Warn("SetServerName has been called after ProcessRequestHeaders")
+	}
+	tx.variables.serverName.Set(serverName)
+}
+
 // ProcessRequestHeaders Performs the analysis on the request readers.
 //
 // This method perform the analysis on the request headers, notice however
@@ -771,7 +775,7 @@ func (tx *Transaction) ProcessRequestHeaders() *types.Interruption {
 func setAndReturnBodyLimitInterruption(tx *Transaction) (*types.Interruption, int, error) {
 	tx.DebugLogger().Warn("Disrupting transaction with body size above the configured limit (Action Reject)")
 	tx.interruption = &types.Interruption{
-		Status: 403,
+		Status: 413,
 		Action: "deny",
 	}
 	return tx.interruption, 0, nil
@@ -954,7 +958,7 @@ func (tx *Transaction) ProcessRequestBody() (*types.Interruption, error) {
 		return nil, err
 	}
 
-	rbp := tx.variables.reqbodyProcessor.String()
+	rbp := tx.variables.reqbodyProcessor.Get()
 
 	// Default variables.ReqbodyProcessor values
 	// XML and JSON must be forced with ctl:requestBodyProcessor=JSON
@@ -1026,7 +1030,7 @@ func (tx *Transaction) ProcessResponseHeaders(code int, proto string) *types.Int
 // directly to the client or write them to Coraza's buffer.
 func (tx *Transaction) IsResponseBodyProcessable() bool {
 	// TODO add more validations
-	ct := tx.variables.responseContentType.String()
+	ct := tx.variables.responseContentType.Get()
 	return stringsutil.InSlice(ct, tx.WAF.ResponseBodyMimeTypes)
 }
 
@@ -1220,7 +1224,7 @@ func (tx *Transaction) ProcessLogging() {
 
 	if tx.AuditEngine == types.AuditEngineRelevantOnly && tx.audit {
 		re := tx.WAF.AuditLogRelevantStatus
-		status := tx.variables.responseStatus.String()
+		status := tx.variables.responseStatus.Get()
 		if re != nil && !re.Match([]byte(status)) {
 			// Not relevant status
 			tx.WAF.Logger.Debug("[%s] Transaction status not marked for audit logging", tx.id)
@@ -1272,35 +1276,38 @@ func (tx *Transaction) AuditLog() *loggers.AuditLog {
 	// YYYY/MM/DD HH:mm:ss
 	ts := time.Unix(0, tx.Timestamp).Format("2006/01/02 15:04:05")
 	al.Parts = tx.AuditLogParts
+	clientPort, _ := strconv.Atoi(tx.variables.remotePort.Get())
+	hostPort, _ := strconv.Atoi(tx.variables.serverPort.Get())
+	status, _ := strconv.Atoi(tx.variables.responseStatus.Get())
 	al.Transaction = loggers.AuditTransaction{
 		Timestamp:     ts,
 		UnixTimestamp: tx.Timestamp,
 		ID:            tx.id,
-		ClientIP:      tx.variables.remoteAddr.String(),
-		ClientPort:    tx.variables.remotePort.Int(),
-		HostIP:        tx.variables.serverAddr.String(),
-		HostPort:      tx.variables.serverPort.Int(),
-		ServerID:      tx.variables.serverName.String(), // TODO check
+		ClientIP:      tx.variables.remoteAddr.Get(),
+		ClientPort:    clientPort,
+		HostIP:        tx.variables.serverAddr.Get(),
+		HostPort:      hostPort,
+		ServerID:      tx.variables.serverName.Get(), // TODO check
 		Request: loggers.AuditTransactionRequest{
-			Method:      tx.variables.requestMethod.String(),
-			Protocol:    tx.variables.requestProtocol.String(),
-			URI:         tx.variables.requestURI.String(),
-			HTTPVersion: tx.variables.requestProtocol.String(),
+			Method:      tx.variables.requestMethod.Get(),
+			Protocol:    tx.variables.requestProtocol.Get(),
+			URI:         tx.variables.requestURI.Get(),
+			HTTPVersion: tx.variables.requestProtocol.Get(),
 			// Body and headers are audit variables.RequestUriRaws
 		},
 		Response: loggers.AuditTransactionResponse{
-			Status: tx.variables.responseStatus.Int(),
+			Status: status,
 			// body and headers are audit parts
 		},
 	}
 	rengine := tx.RuleEngine.String()
 
 	al.Transaction.Request.Headers = tx.variables.requestHeaders.Data()
-	al.Transaction.Request.Body = tx.variables.requestBody.String()
+	al.Transaction.Request.Body = tx.variables.requestBody.Get()
 	// TODO maybe change to:
 	// al.Transaction.Request.Body = tx.RequestBodyBuffer.String()
 	al.Transaction.Response.Headers = tx.variables.responseHeaders.Data()
-	al.Transaction.Response.Body = tx.variables.responseBody.String()
+	al.Transaction.Response.Body = tx.variables.responseBody.Get()
 	al.Transaction.Producer = loggers.AuditTransactionProducer{
 		Connector:  tx.WAF.ProducerConnector,
 		Version:    tx.WAF.ProducerConnectorVersion,
@@ -1389,634 +1396,595 @@ func (tx *Transaction) Close() error {
 	}
 }
 
+// String will return a string with the transaction debug information
 func (tx *Transaction) String() string {
-	return tx.Debug()
-}
-
-// Debug will return a string with the transaction debug information
-func (tx *Transaction) Debug() string {
-	res := "\n\n----------------------- ERRORLOG ----------------------\n"
+	res := strings.Builder{}
+	res.WriteString("\n\n----------------------- ERRORLOG ----------------------\n")
 	for _, mr := range tx.matchedRules {
-		res += mr.ErrorLog(tx.variables.responseStatus.Int())
-		res += "\n\n----------------------- MATCHDATA ---------------------\n"
+		status, _ := strconv.Atoi(tx.variables.responseStatus.Get())
+		res.WriteString(mr.ErrorLog(status))
+		res.WriteString("\n\n----------------------- MATCHDATA ---------------------\n")
 		for _, md := range mr.MatchedDatas() {
-			res += fmt.Sprintf("%+v", md) + "\n"
+			fmt.Fprintf(&res, "%+v\n", md)
 		}
-		res += "\n"
+		res.WriteByte('\n')
 	}
 
-	res += "\n------------------------ DEBUG ------------------------\n"
+	res.WriteString("\n------------------------ DEBUG ------------------------\n")
 	for v := byte(1); v < types.VariablesCount; v++ {
 		vr := variables.RuleVariable(v)
-		if vr.Name() == "UNKNOWN" {
-			continue
-		}
-		data := map[string][]string{}
-		switch col := tx.Collection(vr).(type) {
-		case *collection.Simple:
-			data[""] = []string{
-				col.String(),
-			}
-		case *collection.Map:
-			data = col.Data()
-		case *collection.Proxy:
-			data = col.Data()
-		case *collection.TranslationProxy:
-			data[""] = col.Data()
-		}
-
-		if len(data) == 1 {
-			res += fmt.Sprintf("%s: ", vr.Name())
-		} else {
-			res += fmt.Sprintf("%s:\n", vr.Name())
-		}
-
-		for k, d := range data {
-			if k != "" {
-				res += fmt.Sprintf("    %s: %s\n", k, strings.Join(d, ","))
-			} else {
-				res += fmt.Sprintf("%s\n", strings.Join(d, ","))
-			}
-		}
+		col := tx.Collection(vr)
+		fmt.Fprint(&res, col)
 	}
-	return res
+	return res.String()
 }
 
 // generateReqbodyError generates all the error variables for the request body parser
 func (tx *Transaction) generateReqbodyError(err error) {
 	tx.variables.reqbodyError.Set("1")
-	tx.variables.reqbodyErrorMsg.Set(fmt.Sprintf("%s: %s", tx.variables.reqbodyProcessor.String(), err.Error()))
+	tx.variables.reqbodyErrorMsg.Set(fmt.Sprintf("%s: %s", tx.variables.reqbodyProcessor.Get(), err.Error()))
 	tx.variables.reqbodyProcessorError.Set("1")
 	tx.variables.reqbodyProcessorErrorMsg.Set(string(err.Error()))
 }
 
 // TransactionVariables has pointers to all the variables of the transaction
 type TransactionVariables struct {
-	// Simple Variables
-	userID                        *collection.Simple
-	urlencodedError               *collection.Simple
-	responseContentType           *collection.Simple
-	uniqueID                      *collection.Simple
-	argsCombinedSize              *collection.SizeProxy
-	authType                      *collection.Simple
-	filesCombinedSize             *collection.Simple
-	fullRequest                   *collection.Simple
-	fullRequestLength             *collection.Simple
-	inboundDataError              *collection.Simple
-	matchedVar                    *collection.Simple
-	matchedVarName                *collection.Simple
-	multipartBoundaryQuoted       *collection.Simple
-	multipartBoundaryWhitespace   *collection.Simple
-	multipartCrlfLfLines          *collection.Simple
-	multipartDataAfter            *collection.Simple
-	multipartDataBefore           *collection.Simple
-	multipartFileLimitExceeded    *collection.Simple
-	multipartHeaderFolding        *collection.Simple
-	multipartInvalidHeaderFolding *collection.Simple
-	multipartInvalidPart          *collection.Simple
-	multipartInvalidQuoting       *collection.Simple
-	multipartLfLine               *collection.Simple
-	multipartMissingSemicolon     *collection.Simple
-	multipartStrictError          *collection.Simple
-	multipartUnmatchedBoundary    *collection.Simple
-	outboundDataError             *collection.Simple
-	pathInfo                      *collection.Simple
-	queryString                   *collection.Simple
-	remoteAddr                    *collection.Simple
-	remoteHost                    *collection.Simple
-	remotePort                    *collection.Simple
-	reqbodyError                  *collection.Simple
-	reqbodyErrorMsg               *collection.Simple
-	reqbodyProcessorError         *collection.Simple
-	reqbodyProcessorErrorMsg      *collection.Simple
-	reqbodyProcessor              *collection.Simple
-	requestBasename               *collection.Simple
-	requestBody                   *collection.Simple
-	requestBodyLength             *collection.Simple
-	requestFilename               *collection.Simple
-	requestLine                   *collection.Simple
-	requestMethod                 *collection.Simple
-	requestProtocol               *collection.Simple
-	requestURI                    *collection.Simple
-	requestURIRaw                 *collection.Simple
-	responseBody                  *collection.Simple
-	responseContentLength         *collection.Simple
-	responseProtocol              *collection.Simple
-	responseStatus                *collection.Simple
-	serverAddr                    *collection.Simple
-	serverName                    *collection.Simple
-	serverPort                    *collection.Simple
-	sessionID                     *collection.Simple
-	highestSeverity               *collection.Simple
-	statusLine                    *collection.Simple
-	inboundErrorData              *collection.Simple
+	// Single Variables
+	userID                        *collections.Single
+	urlencodedError               *collections.Single
+	responseContentType           *collections.Single
+	uniqueID                      *collections.Single
+	argsCombinedSize              *collections.SizeCollection
+	authType                      *collections.Single
+	filesCombinedSize             *collections.Single
+	fullRequest                   *collections.Single
+	fullRequestLength             *collections.Single
+	inboundDataError              *collections.Single
+	matchedVar                    *collections.Single
+	matchedVarName                *collections.Single
+	multipartBoundaryQuoted       *collections.Single
+	multipartBoundaryWhitespace   *collections.Single
+	multipartCrlfLfLines          *collections.Single
+	multipartDataAfter            *collections.Single
+	multipartDataBefore           *collections.Single
+	multipartFileLimitExceeded    *collections.Single
+	multipartHeaderFolding        *collections.Single
+	multipartInvalidHeaderFolding *collections.Single
+	multipartInvalidPart          *collections.Single
+	multipartInvalidQuoting       *collections.Single
+	multipartLfLine               *collections.Single
+	multipartMissingSemicolon     *collections.Single
+	multipartStrictError          *collections.Single
+	multipartUnmatchedBoundary    *collections.Single
+	outboundDataError             *collections.Single
+	pathInfo                      *collections.Single
+	queryString                   *collections.Single
+	remoteAddr                    *collections.Single
+	remoteHost                    *collections.Single
+	remotePort                    *collections.Single
+	reqbodyError                  *collections.Single
+	reqbodyErrorMsg               *collections.Single
+	reqbodyProcessorError         *collections.Single
+	reqbodyProcessorErrorMsg      *collections.Single
+	reqbodyProcessor              *collections.Single
+	requestBasename               *collections.Single
+	requestBody                   *collections.Single
+	requestBodyLength             *collections.Single
+	requestFilename               *collections.Single
+	requestLine                   *collections.Single
+	requestMethod                 *collections.Single
+	requestProtocol               *collections.Single
+	requestURI                    *collections.Single
+	requestURIRaw                 *collections.Single
+	responseBody                  *collections.Single
+	responseContentLength         *collections.Single
+	responseProtocol              *collections.Single
+	responseStatus                *collections.Single
+	serverAddr                    *collections.Single
+	serverName                    *collections.Single
+	serverPort                    *collections.Single
+	sessionID                     *collections.Single
+	highestSeverity               *collections.Single
+	statusLine                    *collections.Single
+	inboundErrorData              *collections.Single
 	// Custom
-	env      *collection.Map
-	tx       *collection.Map
-	rule     *collection.Map
-	duration *collection.Simple
-	// Proxy Variables
-	args *collection.Proxy
-	// Maps Variables
-	argsGet              *collection.Map
-	argsPost             *collection.Map
-	argsPath             *collection.Map
-	filesTmpNames        *collection.Map
-	geo                  *collection.Map
-	files                *collection.Map
-	requestCookies       *collection.Map
-	requestHeaders       *collection.Map
-	responseHeaders      *collection.Map
-	multipartName        *collection.Map
-	matchedVarsNames     *collection.Map
-	multipartFilename    *collection.Map
-	matchedVars          *collection.Map
-	filesSizes           *collection.Map
-	filesNames           *collection.Map
-	filesTmpContent      *collection.Map
-	responseHeadersNames *collection.Map
-	requestHeadersNames  *collection.Map
-	requestCookiesNames  *collection.Map
-	xml                  *collection.Map
-	requestXML           *collection.Map
-	responseXML          *collection.Map
-	multipartPartHeaders *collection.Map
+	env                  collection.Map
+	tx                   collection.Map
+	rule                 collection.Map
+	duration             *collections.Single
+	args                 *collections.ConcatKeyed
+	argsGet              *collections.NamedCollection
+	argsGetNames         collection.Collection
+	argsPost             *collections.NamedCollection
+	argsPostNames        collection.Collection
+	argsPath             *collections.NamedCollection
+	argsNames            *collections.ConcatCollection
+	filesTmpNames        collection.Map
+	geo                  collection.Map
+	files                collection.Map
+	requestCookies       *collections.NamedCollection
+	requestCookiesNames  collection.Collection
+	requestHeaders       *collections.NamedCollection
+	responseHeadersNames collection.Collection
+	responseHeaders      *collections.NamedCollection
+	requestHeadersNames  collection.Collection
+	multipartName        collection.Map
+	multipartFilename    collection.Map
+	matchedVars          *collections.NamedCollection
+	matchedVarsNames     collection.Collection
+	filesSizes           collection.Map
+	filesNames           collection.Map
+	filesTmpContent      collection.Map
+	xml                  collection.Map
+	requestXML           collection.Map
+	responseXML          collection.Map
+	multipartPartHeaders collection.Map
 	// Persistent variables
-	ip *collection.Map
-	// Translation Proxy Variables
-	argsNames     *collection.TranslationProxy
-	argsGetNames  *collection.TranslationProxy
-	argsPostNames *collection.TranslationProxy
+	ip collection.Map
 }
 
 func NewTransactionVariables() *TransactionVariables {
 	v := &TransactionVariables{}
-	v.urlencodedError = collection.NewSimple(variables.UrlencodedError)
-	v.responseContentType = collection.NewSimple(variables.ResponseContentType)
-	v.uniqueID = collection.NewSimple(variables.UniqueID)
-	v.authType = collection.NewSimple(variables.AuthType)
-	v.filesCombinedSize = collection.NewSimple(variables.FilesCombinedSize)
-	v.fullRequest = collection.NewSimple(variables.FullRequest)
-	v.fullRequestLength = collection.NewSimple(variables.FullRequestLength)
-	v.inboundDataError = collection.NewSimple(variables.InboundDataError)
-	v.matchedVar = collection.NewSimple(variables.MatchedVar)
-	v.matchedVarName = collection.NewSimple(variables.MatchedVarName)
-	v.multipartBoundaryQuoted = collection.NewSimple(variables.MultipartBoundaryQuoted)
-	v.multipartBoundaryWhitespace = collection.NewSimple(variables.MultipartBoundaryWhitespace)
-	v.multipartCrlfLfLines = collection.NewSimple(variables.MultipartCrlfLfLines)
-	v.multipartDataAfter = collection.NewSimple(variables.MultipartDataAfter)
-	v.multipartDataBefore = collection.NewSimple(variables.MultipartDataBefore)
-	v.multipartFileLimitExceeded = collection.NewSimple(variables.MultipartFileLimitExceeded)
-	v.multipartHeaderFolding = collection.NewSimple(variables.MultipartHeaderFolding)
-	v.multipartInvalidHeaderFolding = collection.NewSimple(variables.MultipartInvalidHeaderFolding)
-	v.multipartInvalidPart = collection.NewSimple(variables.MultipartInvalidPart)
-	v.multipartInvalidQuoting = collection.NewSimple(variables.MultipartInvalidQuoting)
-	v.multipartLfLine = collection.NewSimple(variables.MultipartLfLine)
-	v.multipartMissingSemicolon = collection.NewSimple(variables.MultipartMissingSemicolon)
-	v.multipartStrictError = collection.NewSimple(variables.MultipartStrictError)
-	v.multipartUnmatchedBoundary = collection.NewSimple(variables.MultipartUnmatchedBoundary)
-	v.outboundDataError = collection.NewSimple(variables.OutboundDataError)
-	v.pathInfo = collection.NewSimple(variables.PathInfo)
-	v.queryString = collection.NewSimple(variables.QueryString)
-	v.remoteAddr = collection.NewSimple(variables.RemoteAddr)
-	v.remoteHost = collection.NewSimple(variables.RemoteHost)
-	v.remotePort = collection.NewSimple(variables.RemotePort)
-	v.reqbodyError = collection.NewSimple(variables.ReqbodyError)
-	v.reqbodyErrorMsg = collection.NewSimple(variables.ReqbodyErrorMsg)
-	v.reqbodyProcessorError = collection.NewSimple(variables.ReqbodyProcessorError)
-	v.reqbodyProcessorErrorMsg = collection.NewSimple(variables.ReqbodyProcessorErrorMsg)
-	v.reqbodyProcessor = collection.NewSimple(variables.ReqbodyProcessor)
-	v.requestBasename = collection.NewSimple(variables.RequestBasename)
-	v.requestBody = collection.NewSimple(variables.RequestBody)
-	v.requestBodyLength = collection.NewSimple(variables.RequestBodyLength)
-	v.requestFilename = collection.NewSimple(variables.RequestFilename)
-	v.requestLine = collection.NewSimple(variables.RequestLine)
-	v.requestMethod = collection.NewSimple(variables.RequestMethod)
-	v.requestProtocol = collection.NewSimple(variables.RequestProtocol)
-	v.requestURI = collection.NewSimple(variables.RequestURI)
-	v.requestURIRaw = collection.NewSimple(variables.RequestURIRaw)
-	v.responseBody = collection.NewSimple(variables.ResponseBody)
-	v.responseContentLength = collection.NewSimple(variables.ResponseContentLength)
-	v.responseProtocol = collection.NewSimple(variables.ResponseProtocol)
-	v.responseStatus = collection.NewSimple(variables.ResponseStatus)
-	v.serverAddr = collection.NewSimple(variables.ServerAddr)
-	v.serverName = collection.NewSimple(variables.ServerName)
-	v.serverPort = collection.NewSimple(variables.ServerPort)
-	v.sessionID = collection.NewSimple(variables.Sessionid)
-	v.highestSeverity = collection.NewSimple(variables.HighestSeverity)
-	v.statusLine = collection.NewSimple(variables.StatusLine)
-	v.inboundErrorData = collection.NewSimple(variables.InboundErrorData)
-	v.duration = collection.NewSimple(variables.Duration)
-	v.responseHeadersNames = collection.NewMap(variables.ResponseHeadersNames)
-	v.requestHeadersNames = collection.NewMap(variables.RequestHeadersNames)
-	v.userID = collection.NewSimple(variables.Userid)
-
-	v.argsGet = collection.NewMap(variables.ArgsGet)
-	v.argsPost = collection.NewMap(variables.ArgsPost)
-	v.argsPath = collection.NewMap(variables.ArgsPath)
-	v.filesSizes = collection.NewMap(variables.FilesSizes)
-	v.filesTmpContent = collection.NewMap(variables.FilesTmpContent)
-	v.multipartFilename = collection.NewMap(variables.MultipartFilename)
-	v.multipartName = collection.NewMap(variables.MultipartName)
-	v.matchedVars = collection.NewMap(variables.MatchedVars)
-	v.requestCookies = collection.NewMap(variables.RequestCookies)
-	v.requestHeaders = collection.NewMap(variables.RequestHeaders)
-	v.responseHeaders = collection.NewMap(variables.ResponseHeaders)
-	v.geo = collection.NewMap(variables.Geo)
-	v.tx = collection.NewMap(variables.TX)
-	v.rule = collection.NewMap(variables.Rule)
-	v.env = collection.NewMap(variables.Env)
-	v.ip = collection.NewMap(variables.IP)
-	v.files = collection.NewMap(variables.Files)
-	v.matchedVarsNames = collection.NewMap(variables.MatchedVarsNames)
-	v.filesNames = collection.NewMap(variables.FilesNames)
-	v.filesTmpNames = collection.NewMap(variables.FilesTmpNames)
-	v.requestCookiesNames = collection.NewMap(variables.RequestCookiesNames)
-	v.responseXML = collection.NewMap(variables.ResponseXML)
-	v.requestXML = collection.NewMap(variables.RequestXML)
-	v.multipartPartHeaders = collection.NewMap(variables.MultipartPartHeaders)
-
-	v.argsCombinedSize = collection.NewCollectionSizeProxy(variables.ArgsCombinedSize, v.argsGet, v.argsPost)
+	v.urlencodedError = collections.NewSingle(variables.UrlencodedError)
+	v.responseContentType = collections.NewSingle(variables.ResponseContentType)
+	v.uniqueID = collections.NewSingle(variables.UniqueID)
+	v.authType = collections.NewSingle(variables.AuthType)
+	v.filesCombinedSize = collections.NewSingle(variables.FilesCombinedSize)
+	v.fullRequest = collections.NewSingle(variables.FullRequest)
+	v.fullRequestLength = collections.NewSingle(variables.FullRequestLength)
+	v.inboundDataError = collections.NewSingle(variables.InboundDataError)
+	v.matchedVar = collections.NewSingle(variables.MatchedVar)
+	v.matchedVarName = collections.NewSingle(variables.MatchedVarName)
+	v.multipartBoundaryQuoted = collections.NewSingle(variables.MultipartBoundaryQuoted)
+	v.multipartBoundaryWhitespace = collections.NewSingle(variables.MultipartBoundaryWhitespace)
+	v.multipartCrlfLfLines = collections.NewSingle(variables.MultipartCrlfLfLines)
+	v.multipartDataAfter = collections.NewSingle(variables.MultipartDataAfter)
+	v.multipartDataBefore = collections.NewSingle(variables.MultipartDataBefore)
+	v.multipartFileLimitExceeded = collections.NewSingle(variables.MultipartFileLimitExceeded)
+	v.multipartHeaderFolding = collections.NewSingle(variables.MultipartHeaderFolding)
+	v.multipartInvalidHeaderFolding = collections.NewSingle(variables.MultipartInvalidHeaderFolding)
+	v.multipartInvalidPart = collections.NewSingle(variables.MultipartInvalidPart)
+	v.multipartInvalidQuoting = collections.NewSingle(variables.MultipartInvalidQuoting)
+	v.multipartLfLine = collections.NewSingle(variables.MultipartLfLine)
+	v.multipartMissingSemicolon = collections.NewSingle(variables.MultipartMissingSemicolon)
+	v.multipartStrictError = collections.NewSingle(variables.MultipartStrictError)
+	v.multipartUnmatchedBoundary = collections.NewSingle(variables.MultipartUnmatchedBoundary)
+	v.outboundDataError = collections.NewSingle(variables.OutboundDataError)
+	v.pathInfo = collections.NewSingle(variables.PathInfo)
+	v.queryString = collections.NewSingle(variables.QueryString)
+	v.remoteAddr = collections.NewSingle(variables.RemoteAddr)
+	v.remoteHost = collections.NewSingle(variables.RemoteHost)
+	v.remotePort = collections.NewSingle(variables.RemotePort)
+	v.reqbodyError = collections.NewSingle(variables.ReqbodyError)
+	v.reqbodyErrorMsg = collections.NewSingle(variables.ReqbodyErrorMsg)
+	v.reqbodyProcessorError = collections.NewSingle(variables.ReqbodyProcessorError)
+	v.reqbodyProcessorErrorMsg = collections.NewSingle(variables.ReqbodyProcessorErrorMsg)
+	v.reqbodyProcessor = collections.NewSingle(variables.ReqbodyProcessor)
+	v.requestBasename = collections.NewSingle(variables.RequestBasename)
+	v.requestBody = collections.NewSingle(variables.RequestBody)
+	v.requestBodyLength = collections.NewSingle(variables.RequestBodyLength)
+	v.requestFilename = collections.NewSingle(variables.RequestFilename)
+	v.requestLine = collections.NewSingle(variables.RequestLine)
+	v.requestMethod = collections.NewSingle(variables.RequestMethod)
+	v.requestProtocol = collections.NewSingle(variables.RequestProtocol)
+	v.requestURI = collections.NewSingle(variables.RequestURI)
+	v.requestURIRaw = collections.NewSingle(variables.RequestURIRaw)
+	v.responseBody = collections.NewSingle(variables.ResponseBody)
+	v.responseContentLength = collections.NewSingle(variables.ResponseContentLength)
+	v.responseProtocol = collections.NewSingle(variables.ResponseProtocol)
+	v.responseStatus = collections.NewSingle(variables.ResponseStatus)
+	v.serverAddr = collections.NewSingle(variables.ServerAddr)
+	v.serverName = collections.NewSingle(variables.ServerName)
+	v.serverPort = collections.NewSingle(variables.ServerPort)
+	v.sessionID = collections.NewSingle(variables.Sessionid)
+	v.highestSeverity = collections.NewSingle(variables.HighestSeverity)
+	v.statusLine = collections.NewSingle(variables.StatusLine)
+	v.inboundErrorData = collections.NewSingle(variables.InboundErrorData)
+	v.duration = collections.NewSingle(variables.Duration)
+	v.userID = collections.NewSingle(variables.Userid)
+
+	v.filesSizes = collections.NewMap(variables.FilesSizes)
+	v.filesTmpContent = collections.NewMap(variables.FilesTmpContent)
+	v.multipartFilename = collections.NewMap(variables.MultipartFilename)
+	v.multipartName = collections.NewMap(variables.MultipartName)
+	v.matchedVars = collections.NewNamedCollection(variables.MatchedVars)
+	v.matchedVarsNames = v.matchedVars.Names(variables.MatchedVarsNames)
+	v.requestCookies = collections.NewNamedCollection(variables.RequestCookies)
+	v.requestCookiesNames = v.requestCookies.Names(variables.RequestCookiesNames)
+	v.requestHeaders = collections.NewNamedCollection(variables.RequestHeaders)
+	v.requestHeadersNames = v.requestHeaders.Names(variables.RequestHeadersNames)
+	v.responseHeaders = collections.NewNamedCollection(variables.ResponseHeaders)
+	v.responseHeadersNames = v.responseHeaders.Names(variables.ResponseHeadersNames)
+	v.geo = collections.NewMap(variables.Geo)
+	v.tx = collections.NewMap(variables.TX)
+	v.rule = collections.NewMap(variables.Rule)
+	v.env = collections.NewMap(variables.Env)
+	v.ip = collections.NewMap(variables.IP)
+	v.files = collections.NewMap(variables.Files)
+	v.filesNames = collections.NewMap(variables.FilesNames)
+	v.filesTmpNames = collections.NewMap(variables.FilesTmpNames)
+	v.responseXML = collections.NewMap(variables.ResponseXML)
+	v.requestXML = collections.NewMap(variables.RequestXML)
+	v.multipartPartHeaders = collections.NewMap(variables.MultipartPartHeaders)
 
 	// XML is a pointer to RequestXML
 	v.xml = v.requestXML
-	v.args = collection.NewProxy(
+
+	v.argsGet = collections.NewNamedCollection(variables.ArgsGet)
+	v.argsGetNames = v.argsGet.Names(variables.ArgsGetNames)
+	v.argsPost = collections.NewNamedCollection(variables.ArgsPost)
+	v.argsPostNames = v.argsPost.Names(variables.ArgsPostNames)
+	v.argsPath = collections.NewNamedCollection(variables.ArgsPath)
+	v.argsCombinedSize = collections.NewSizeCollection(variables.ArgsCombinedSize, v.argsGet, v.argsPost)
+	v.args = collections.NewConcatKeyed(
 		variables.Args,
 		v.argsGet,
 		v.argsPost,
 		v.argsPath,
 	)
-
-	v.argsNames = collection.NewTranslationProxy(
+	v.argsNames = collections.NewConcatCollection(
 		variables.ArgsNames,
-		v.argsGet,
-		v.argsPost,
-		v.argsPath,
-	)
-	v.argsGetNames = collection.NewTranslationProxy(
-		variables.ArgsGetNames,
-		v.argsGet,
-	)
-	v.argsPostNames = collection.NewTranslationProxy(
-		variables.ArgsPostNames,
-		v.argsPost,
+		v.argsGetNames,
+		v.argsPostNames,
+		// Only used in a concatenating collection so variable name doesn't matter.
+		v.argsPath.Names(variables.Unknown),
 	)
 	return v
 }
 
-func (v *TransactionVariables) UserID() *collection.Simple {
+func (v *TransactionVariables) UserID() collection.Single {
 	return v.userID
 }
 
-func (v *TransactionVariables) UrlencodedError() *collection.Simple {
+func (v *TransactionVariables) UrlencodedError() collection.Single {
 	return v.urlencodedError
 }
 
-func (v *TransactionVariables) ResponseContentType() *collection.Simple {
+func (v *TransactionVariables) ResponseContentType() collection.Single {
 	return v.responseContentType
 }
 
-func (v *TransactionVariables) UniqueID() *collection.Simple {
+func (v *TransactionVariables) UniqueID() collection.Single {
 	return v.uniqueID
 }
 
-func (v *TransactionVariables) ArgsCombinedSize() *collection.SizeProxy {
+func (v *TransactionVariables) ArgsCombinedSize() collection.Collection {
 	return v.argsCombinedSize
 }
 
-func (v *TransactionVariables) AuthType() *collection.Simple {
+func (v *TransactionVariables) AuthType() collection.Single {
 	return v.authType
 }
 
-func (v *TransactionVariables) FilesCombinedSize() *collection.Simple {
+func (v *TransactionVariables) FilesCombinedSize() collection.Single {
 	return v.filesCombinedSize
 }
 
-func (v *TransactionVariables) FullRequest() *collection.Simple {
+func (v *TransactionVariables) FullRequest() collection.Single {
 	return v.fullRequest
 }
 
-func (v *TransactionVariables) FullRequestLength() *collection.Simple {
+func (v *TransactionVariables) FullRequestLength() collection.Single {
 	return v.fullRequestLength
 }
 
-func (v *TransactionVariables) InboundDataError() *collection.Simple {
+func (v *TransactionVariables) InboundDataError() collection.Single {
 	return v.inboundDataError
 }
 
-func (v *TransactionVariables) MatchedVar() *collection.Simple {
+func (v *TransactionVariables) MatchedVar() collection.Single {
 	return v.matchedVar
 }
 
-func (v *TransactionVariables) MatchedVarName() *collection.Simple {
+func (v *TransactionVariables) MatchedVarName() collection.Single {
 	return v.matchedVarName
 }
 
-func (v *TransactionVariables) MultipartBoundaryQuoted() *collection.Simple {
+func (v *TransactionVariables) MultipartBoundaryQuoted() collection.Single {
 	return v.multipartBoundaryQuoted
 }
 
-func (v *TransactionVariables) MultipartBoundaryWhitespace() *collection.Simple {
+func (v *TransactionVariables) MultipartBoundaryWhitespace() collection.Single {
 	return v.multipartBoundaryWhitespace
 }
 
-func (v *TransactionVariables) MultipartCrlfLfLines() *collection.Simple {
+func (v *TransactionVariables) MultipartCrlfLfLines() collection.Single {
 	return v.multipartCrlfLfLines
 }
 
-func (v *TransactionVariables) MultipartDataAfter() *collection.Simple {
+func (v *TransactionVariables) MultipartDataAfter() collection.Single {
 	return v.multipartDataAfter
 }
 
-func (v *TransactionVariables) MultipartDataBefore() *collection.Simple {
+func (v *TransactionVariables) MultipartDataBefore() collection.Single {
 	return v.multipartDataBefore
 }
 
-func (v *TransactionVariables) MultipartFileLimitExceeded() *collection.Simple {
+func (v *TransactionVariables) MultipartFileLimitExceeded() collection.Single {
 	return v.multipartFileLimitExceeded
 }
 
-func (v *TransactionVariables) MultipartHeaderFolding() *collection.Simple {
+func (v *TransactionVariables) MultipartHeaderFolding() collection.Single {
 	return v.multipartHeaderFolding
 }
 
-func (v *TransactionVariables) MultipartInvalidHeaderFolding() *collection.Simple {
+func (v *TransactionVariables) MultipartInvalidHeaderFolding() collection.Single {
 	return v.multipartInvalidHeaderFolding
 }
 
-func (v *TransactionVariables) MultipartInvalidPart() *collection.Simple {
+func (v *TransactionVariables) MultipartInvalidPart() collection.Single {
 	return v.multipartInvalidPart
 }
 
-func (v *TransactionVariables) MultipartInvalidQuoting() *collection.Simple {
+func (v *TransactionVariables) MultipartInvalidQuoting() collection.Single {
 	return v.multipartInvalidQuoting
 }
 
-func (v *TransactionVariables) MultipartLfLine() *collection.Simple {
+func (v *TransactionVariables) MultipartLfLine() collection.Single {
 	return v.multipartLfLine
 }
 
-func (v *TransactionVariables) MultipartMissingSemicolon() *collection.Simple {
+func (v *TransactionVariables) MultipartMissingSemicolon() collection.Single {
 	return v.multipartMissingSemicolon
 }
 
-func (v *TransactionVariables) MultipartPartHeaders() *collection.Map {
+func (v *TransactionVariables) MultipartPartHeaders() collection.Map {
 	return v.multipartPartHeaders
 }
 
-func (v *TransactionVariables) MultipartStrictError() *collection.Simple {
+func (v *TransactionVariables) MultipartStrictError() collection.Single {
 	return v.multipartStrictError
 }
 
-func (v *TransactionVariables) MultipartUnmatchedBoundary() *collection.Simple {
+func (v *TransactionVariables) MultipartUnmatchedBoundary() collection.Single {
 	return v.multipartUnmatchedBoundary
 }
 
-func (v *TransactionVariables) OutboundDataError() *collection.Simple {
+func (v *TransactionVariables) OutboundDataError() collection.Single {
 	return v.outboundDataError
 }
 
-func (v *TransactionVariables) PathInfo() *collection.Simple {
+func (v *TransactionVariables) PathInfo() collection.Single {
 	return v.pathInfo
 }
 
-func (v *TransactionVariables) QueryString() *collection.Simple {
+func (v *TransactionVariables) QueryString() collection.Single {
 	return v.queryString
 }
 
-func (v *TransactionVariables) RemoteAddr() *collection.Simple {
+func (v *TransactionVariables) RemoteAddr() collection.Single {
 	return v.remoteAddr
 }
 
-func (v *TransactionVariables) RemoteHost() *collection.Simple {
+func (v *TransactionVariables) RemoteHost() collection.Single {
 	return v.remoteHost
 }
 
-func (v *TransactionVariables) RemotePort() *collection.Simple {
+func (v *TransactionVariables) RemotePort() collection.Single {
 	return v.remotePort
 }
 
-func (v *TransactionVariables) RequestBodyError() *collection.Simple {
+func (v *TransactionVariables) RequestBodyError() collection.Single {
 	return v.reqbodyError
 }
 
-func (v *TransactionVariables) RequestBodyErrorMsg() *collection.Simple {
+func (v *TransactionVariables) RequestBodyErrorMsg() collection.Single {
 	return v.reqbodyErrorMsg
 }
 
-func (v *TransactionVariables) RequestBodyProcessorError() *collection.Simple {
+func (v *TransactionVariables) RequestBodyProcessorError() collection.Single {
 	return v.reqbodyProcessorError
 }
 
-func (v *TransactionVariables) RequestBodyProcessorErrorMsg() *collection.Simple {
+func (v *TransactionVariables) RequestBodyProcessorErrorMsg() collection.Single {
 	return v.reqbodyProcessorErrorMsg
 }
 
-func (v *TransactionVariables) RequestBodyProcessor() *collection.Simple {
+func (v *TransactionVariables) RequestBodyProcessor() collection.Single {
 	return v.reqbodyProcessor
 }
 
-func (v *TransactionVariables) RequestBasename() *collection.Simple {
+func (v *TransactionVariables) RequestBasename() collection.Single {
 	return v.requestBasename
 }
 
-func (v *TransactionVariables) RequestBody() *collection.Simple {
+func (v *TransactionVariables) RequestBody() collection.Single {
 	return v.requestBody
 }
 
-func (v *TransactionVariables) RequestBodyLength() *collection.Simple {
+func (v *TransactionVariables) RequestBodyLength() collection.Single {
 	return v.requestBodyLength
 }
 
-func (v *TransactionVariables) RequestFilename() *collection.Simple {
+func (v *TransactionVariables) RequestFilename() collection.Single {
 	return v.requestFilename
 }
 
-func (v *TransactionVariables) RequestLine() *collection.Simple {
+func (v *TransactionVariables) RequestLine() collection.Single {
 	return v.requestLine
 }
 
-func (v *TransactionVariables) RequestMethod() *collection.Simple {
+func (v *TransactionVariables) RequestMethod() collection.Single {
 	return v.requestMethod
 }
 
-func (v *TransactionVariables) RequestProtocol() *collection.Simple {
+func (v *TransactionVariables) RequestProtocol() collection.Single {
 	return v.requestProtocol
 }
 
-func (v *TransactionVariables) RequestURI() *collection.Simple {
+func (v *TransactionVariables) RequestURI() collection.Single {
 	return v.requestURI
 }
 
-func (v *TransactionVariables) RequestURIRaw() *collection.Simple {
+func (v *TransactionVariables) RequestURIRaw() collection.Single {
 	return v.requestURIRaw
 }
 
-func (v *TransactionVariables) ResponseBody() *collection.Simple {
+func (v *TransactionVariables) ResponseBody() collection.Single {
 	return v.responseBody
 }
 
-func (v *TransactionVariables) ResponseContentLength() *collection.Simple {
+func (v *TransactionVariables) ResponseContentLength() collection.Single {
 	return v.responseContentLength
 }
 
-func (v *TransactionVariables) ResponseProtocol() *collection.Simple {
+func (v *TransactionVariables) ResponseProtocol() collection.Single {
 	return v.responseProtocol
 }
 
-func (v *TransactionVariables) ResponseStatus() *collection.Simple {
+func (v *TransactionVariables) ResponseStatus() collection.Single {
 	return v.responseStatus
 }
 
-func (v *TransactionVariables) ServerAddr() *collection.Simple {
+func (v *TransactionVariables) ServerAddr() collection.Single {
 	return v.serverAddr
 }
 
-func (v *TransactionVariables) ServerName() *collection.Simple {
+func (v *TransactionVariables) ServerName() collection.Single {
 	return v.serverName
 }
 
-func (v *TransactionVariables) ServerPort() *collection.Simple {
+func (v *TransactionVariables) ServerPort() collection.Single {
 	return v.serverPort
 }
 
-func (v *TransactionVariables) SessionID() *collection.Simple {
+func (v *TransactionVariables) SessionID() collection.Single {
 	return v.sessionID
 }
 
-func (v *TransactionVariables) HighestSeverity() *collection.Simple {
+func (v *TransactionVariables) HighestSeverity() collection.Single {
 	return v.highestSeverity
 }
 
-func (v *TransactionVariables) StatusLine() *collection.Simple {
+func (v *TransactionVariables) StatusLine() collection.Single {
 	return v.statusLine
 }
 
-func (v *TransactionVariables) InboundErrorData() *collection.Simple {
+func (v *TransactionVariables) InboundErrorData() collection.Single {
 	return v.inboundErrorData
 }
 
-func (v *TransactionVariables) Env() *collection.Map {
+func (v *TransactionVariables) Env() collection.Map {
 	return v.env
 }
 
-func (v *TransactionVariables) TX() *collection.Map {
+func (v *TransactionVariables) TX() collection.Map {
 	return v.tx
 }
 
-func (v *TransactionVariables) Rule() *collection.Map {
+func (v *TransactionVariables) Rule() collection.Map {
 	return v.rule
 }
 
-func (v *TransactionVariables) Duration() *collection.Simple {
+func (v *TransactionVariables) Duration() collection.Single {
 	return v.duration
 }
 
-func (v *TransactionVariables) Args() *collection.Proxy {
+func (v *TransactionVariables) Args() collection.Keyed {
 	return v.args
 }
 
-func (v *TransactionVariables) ArgsGet() *collection.Map {
+func (v *TransactionVariables) ArgsGet() collection.Map {
 	return v.argsGet
 }
 
-func (v *TransactionVariables) ArgsPost() *collection.Map {
+func (v *TransactionVariables) ArgsPost() collection.Map {
 	return v.argsPost
 }
 
-func (v *TransactionVariables) ArgsPath() *collection.Map {
+func (v *TransactionVariables) ArgsPath() collection.Map {
 	return v.argsPath
 }
 
-func (v *TransactionVariables) FilesTmpNames() *collection.Map {
+func (v *TransactionVariables) FilesTmpNames() collection.Map {
 	return v.filesTmpNames
 }
 
-func (v *TransactionVariables) Geo() *collection.Map {
+func (v *TransactionVariables) Geo() collection.Map {
 	return v.geo
 }
 
-func (v *TransactionVariables) Files() *collection.Map {
+func (v *TransactionVariables) Files() collection.Map {
 	return v.files
 }
 
-func (v *TransactionVariables) RequestCookies() *collection.Map {
+func (v *TransactionVariables) RequestCookies() collection.Map {
 	return v.requestCookies
 }
 
-func (v *TransactionVariables) RequestHeaders() *collection.Map {
+func (v *TransactionVariables) RequestHeaders() collection.Map {
 	return v.requestHeaders
 }
 
-func (v *TransactionVariables) ResponseHeaders() *collection.Map {
+func (v *TransactionVariables) ResponseHeaders() collection.Map {
 	return v.responseHeaders
 }
 
-func (v *TransactionVariables) MultipartName() *collection.Map {
+func (v *TransactionVariables) MultipartName() collection.Map {
 	return v.multipartName
 }
 
-func (v *TransactionVariables) MatchedVarsNames() *collection.Map {
+func (v *TransactionVariables) MatchedVarsNames() collection.Collection {
 	return v.matchedVarsNames
 }
 
-func (v *TransactionVariables) MultipartFilename() *collection.Map {
+func (v *TransactionVariables) MultipartFilename() collection.Map {
 	return v.multipartFilename
 }
 
-func (v *TransactionVariables) MatchedVars() *collection.Map {
+func (v *TransactionVariables) MatchedVars() collection.Map {
 	return v.matchedVars
 }
 
-func (v *TransactionVariables) FilesSizes() *collection.Map {
+func (v *TransactionVariables) FilesSizes() collection.Map {
 	return v.filesSizes
 }
 
-func (v *TransactionVariables) FilesNames() *collection.Map {
+func (v *TransactionVariables) FilesNames() collection.Map {
 	return v.filesNames
 }
 
-func (v *TransactionVariables) FilesTmpContent() *collection.Map {
+func (v *TransactionVariables) FilesTmpContent() collection.Map {
 	return v.filesTmpContent
 }
 
-func (v *TransactionVariables) ResponseHeadersNames() *collection.Map {
+func (v *TransactionVariables) ResponseHeadersNames() collection.Collection {
 	return v.responseHeadersNames
 }
 
-func (v *TransactionVariables) RequestHeadersNames() *collection.Map {
+func (v *TransactionVariables) RequestHeadersNames() collection.Collection {
 	return v.requestHeadersNames
 }
 
-func (v *TransactionVariables) RequestCookiesNames() *collection.Map {
+func (v *TransactionVariables) RequestCookiesNames() collection.Collection {
 	return v.requestCookiesNames
 }
 
-func (v *TransactionVariables) XML() *collection.Map {
+func (v *TransactionVariables) XML() collection.Map {
 	return v.xml
 }
 
-func (v *TransactionVariables) RequestXML() *collection.Map {
+func (v *TransactionVariables) RequestXML() collection.Map {
 	return v.requestXML
 }
 
-func (v *TransactionVariables) ResponseXML() *collection.Map {
+func (v *TransactionVariables) ResponseXML() collection.Map {
 	return v.responseXML
 }
 
-func (v *TransactionVariables) IP() *collection.Map {
+func (v *TransactionVariables) IP() collection.Map {
 	return v.ip
 }
 
-func (v *TransactionVariables) ArgsNames() *collection.TranslationProxy {
+func (v *TransactionVariables) ArgsNames() collection.Collection {
 	return v.argsNames
 }
 
-func (v *TransactionVariables) ArgsGetNames() *collection.TranslationProxy {
+func (v *TransactionVariables) ArgsGetNames() collection.Collection {
 	return v.argsGetNames
 }
 
-func (v *TransactionVariables) ArgsPostNames() *collection.TranslationProxy {
+func (v *TransactionVariables) ArgsPostNames() collection.Collection {
 	return v.argsPostNames
 }
 
diff --git a/internal/corazawaf/waf.go b/internal/corazawaf/waf.go
index 653bccb..1bade4d 100644
--- a/internal/corazawaf/waf.go
+++ b/internal/corazawaf/waf.go
@@ -4,6 +4,7 @@
 package corazawaf
 
 import (
+	"errors"
 	"fmt"
 	"io"
 	"io/fs"
@@ -49,7 +50,7 @@ type WAF struct {
 	RequestBodyLimit int64
 
 	// Request body in memory limit
-	RequestBodyInMemoryLimit int64
+	requestBodyInMemoryLimit *int64
 
 	// If true, transactions will have access to the response body
 	ResponseBodyAccess bool
@@ -149,9 +150,9 @@ func (w *WAF) newTransactionWithID(id string) *Transaction {
 	tx.AuditLogParts = w.AuditLogParts
 	tx.ForceRequestBodyVariable = false
 	tx.RequestBodyAccess = w.RequestBodyAccess
-	tx.RequestBodyLimit = w.RequestBodyLimit
+	tx.RequestBodyLimit = int64(w.RequestBodyLimit)
 	tx.ResponseBodyAccess = w.ResponseBodyAccess
-	tx.ResponseBodyLimit = w.ResponseBodyLimit
+	tx.ResponseBodyLimit = int64(w.ResponseBodyLimit)
 	tx.RuleEngine = w.RuleEngine
 	tx.HashEngine = false
 	tx.HashEnforcement = false
@@ -169,10 +170,16 @@ func (w *WAF) newTransactionWithID(id string) *Transaction {
 	// Always non-nil if buffers / collections were already initialized so we don't do any of them
 	// based on the presence of RequestBodyBuffer.
 	if tx.requestBodyBuffer == nil {
+		// if no requestBodyInMemoryLimit has been set we default to the
+		var requestBodyInMemoryLimit int64 = w.RequestBodyLimit
+		if w.requestBodyInMemoryLimit != nil {
+			requestBodyInMemoryLimit = int64(*w.requestBodyInMemoryLimit)
+		}
+
 		tx.requestBodyBuffer = NewBodyBuffer(types.BodyBufferOptions{
 			TmpPath:     w.TmpDir,
-			MemoryLimit: w.RequestBodyInMemoryLimit,
-			Limit:       w.ResponseBodyLimit,
+			MemoryLimit: requestBodyInMemoryLimit,
+			Limit:       w.RequestBodyLimit,
 		})
 		tx.responseBodyBuffer = NewBodyBuffer(types.BodyBufferOptions{
 			TmpPath: w.TmpDir,
@@ -242,7 +249,7 @@ func (w *WAF) SetDebugLogPath(path string) error {
 
 	f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
 	if err != nil {
-		w.Logger.Error("failed to open the file: %s", err.Error())
+		w.Logger.Error("failed to open the debug log file: %s", err.Error())
 	}
 
 	w.Logger.SetOutput(f)
@@ -250,41 +257,34 @@ func (w *WAF) SetDebugLogPath(path string) error {
 	return nil
 }
 
+const _1gb = 1073741824
+
 // NewWAF creates a new WAF instance with default variables
 func NewWAF() *WAF {
 	logger := &stdDebugLogger{
 		logger: &log.Logger{},
 		Level:  loggers.LogLevelInfo,
 	}
+
 	logWriter, err := loggers.GetLogWriter("serial")
 	if err != nil {
 		logger.Error("error creating serial log writer: %s", err.Error())
 	}
+
 	waf := &WAF{
 		// Initializing pool for transactions
-		txPool:                   sync.NewPool(func() interface{} { return new(Transaction) }),
-		ArgumentSeparator:        "&",
-		AuditLogWriter:           logWriter,
-		AuditEngine:              types.AuditEngineOff,
-		AuditLogParts:            types.AuditLogParts("ABCFHZ"),
-		RequestBodyAccess:        false,
-		RequestBodyInMemoryLimit: 131072,
-		RequestBodyLimit:         134217728, // 10mb
-		RequestBodyLimitAction:   types.BodyLimitActionReject,
-		ResponseBodyMimeTypes:    []string{"text/html", "text/plain"},
-		ResponseBodyLimit:        524288,
-		ResponseBodyLimitAction:  types.BodyLimitActionReject,
-		ResponseBodyAccess:       false,
-		RuleEngine:               types.RuleEngineOn,
-		Rules:                    NewRuleGroup(),
-		TmpDir:                   "/tmp",
-		AuditLogRelevantStatus:   regexp.MustCompile(`.*`),
-		Logger:                   logger,
-	}
-	// We initialize a basic audit log writer that discards output
-	if err := logWriter.Init(types.Config{}); err != nil {
-		fmt.Println(err)
+		txPool: sync.NewPool(func() interface{} { return new(Transaction) }),
+		// These defaults are unavoidable as they are zero values for the variables
+		RuleEngine:         types.RuleEngineOn,
+		RequestBodyAccess:  false,
+		RequestBodyLimit:   _1gb,
+		ResponseBodyAccess: false,
+		ResponseBodyLimit:  _1gb,
+		AuditLogWriter:     logWriter,
+		Logger:             logger,
+		TmpDir:             os.TempDir(),
 	}
+
 	if err := waf.SetDebugLogPath(""); err != nil {
 		fmt.Println(err)
 	}
@@ -305,3 +305,42 @@ func (w *WAF) SetDebugLogLevel(lvl int) error {
 func (w *WAF) SetErrorCallback(cb func(rule types.MatchedRule)) {
 	w.ErrorLogCb = cb
 }
+
+func (w *WAF) SetRequestBodyInMemoryLimit(limit int64) {
+	w.requestBodyInMemoryLimit = &limit
+}
+
+func (w *WAF) RequestBodyInMemoryLimit() *int64 {
+	return w.requestBodyInMemoryLimit
+}
+
+// Validate validates the waf after all the settings have been set.
+func (w *WAF) Validate() error {
+	if w.RequestBodyLimit <= 0 {
+		return errors.New("request body limit should be bigger than 0")
+	}
+
+	if w.RequestBodyLimit > _1gb {
+		return errors.New("request body limit should be at most 1GB")
+	}
+
+	if w.requestBodyInMemoryLimit != nil {
+		if *w.requestBodyInMemoryLimit <= 0 {
+			return errors.New("request body memory limit should be bigger than 0")
+		}
+
+		if w.RequestBodyLimit < *w.requestBodyInMemoryLimit {
+			return fmt.Errorf("request body limit should be at least the memory limit")
+		}
+	}
+
+	if w.ResponseBodyLimit <= 0 {
+		return errors.New("response body limit should be bigger than 0")
+	}
+
+	if w.ResponseBodyLimit > _1gb {
+		return errors.New("response body limit should be at most 1GB")
+	}
+
+	return nil
+}
diff --git a/internal/seclang/directives.go b/internal/seclang/directives.go
index ad8fb18..0459e05 100644
--- a/internal/seclang/directives.go
+++ b/internal/seclang/directives.go
@@ -14,6 +14,7 @@ import (
 	"strings"
 
 	"github.com/corazawaf/coraza/v3/internal/corazawaf"
+	utils "github.com/corazawaf/coraza/v3/internal/strings"
 	"github.com/corazawaf/coraza/v3/loggers"
 	"github.com/corazawaf/coraza/v3/types"
 )
@@ -49,13 +50,18 @@ func directiveInclude(_ *DirectiveOptions) error {
 
 var _ directive = directiveInclude
 
+var errEmptyOptions = errors.New("expected options")
+
 func directiveSecComponentSignature(options *DirectiveOptions) error {
+	if len(options.Opts) == 0 {
+		return errEmptyOptions
+	}
 	options.WAF.ComponentNames = append(options.WAF.ComponentNames, options.Opts)
 	return nil
 }
 
-// Description: Adds a fixed rule marker that can be used as a target in a skipAfter action.
-// A SecMarker directive essentially creates a rule that does nothing and whose only purpose
+// Description: Adds a fixed rule marker that can be used as a target in a `skipAfter` action.
+// A `SecMarker` directive essentially creates a rule that does nothing and whose only purpose
 // is to carry the given ID.
 // Syntax: SecMarker [ID|TEXT]
 // ---
@@ -63,29 +69,34 @@ func directiveSecComponentSignature(options *DirectiveOptions) error {
 // allow you to choose the best way to implement a skip-over. Here is an example used from the
 // Core Rule Set:
 //
-// ```
-// SecMarker BEGIN_HOST_CHECK
+// ```apache
 //
-//	SecRule &REQUEST_HEADERS:Host "@eq 0" \
-//		   "id:'960008',skipAfter:END_HOST_CHECK,phase:2,rev:'2.1.1',\
-//		   t:none,block,msg:'Request Missing a Host Header',\
-//		   tag:'PROTOCOL_VIOLATION/MISSING_HEADER_HOST',tag:'WASCTC/WASC-21',\
-//		   tag:'OWASP_TOP_10/A7',tag:'PCI/6.5.10',\
-//		   severity:'5',setvar:'tx.msg=%{rule.msg}',setvar:tx.anomaly_score=+%{tx.notice_anomaly_score},\
-//		   setvar:tx.protocol_violation_score=+%{tx.notice_anomaly_score},\
-//		   setvar:tx.%{rule.id}-PROTOCOL_VIOLATION/MISSING_HEADER-%{matched_var_name}=%{matched_var}"
+//	SecMarker BEGIN_HOST_CHECK
 //
+//	SecRule &REQUEST_HEADERS:Host "@eq 0" \
+//		"id:'960008',skipAfter:END_HOST_CHECK,phase:2,rev:'2.1.1',\
+//		t:none,block,msg:'Request Missing a Host Header',\
+//		tag:'PROTOCOL_VIOLATION/MISSING_HEADER_HOST',tag:'WASCTC/WASC-21',\
+//		tag:'OWASP_TOP_10/A7',tag:'PCI/6.5.10',\
+//		severity:'5',setvar:'tx.msg=%{rule.msg}',setvar:tx.anomaly_score=+%{tx.notice_anomaly_score},\
+//		setvar:tx.protocol_violation_score=+%{tx.notice_anomaly_score},\
+//		setvar:tx.%{rule.id}-PROTOCOL_VIOLATION/MISSING_HEADER-%{matched_var_name}=%{matched_var}"
 //	SecRule REQUEST_HEADERS:Host "^$" \
-//		   "id:'960008',phase:2,rev:'2.1.1',t:none,block,msg:'Request Missing a Host Header',\
-//		   tag:'PROTOCOL_VIOLATION/MISSING_HEADER_HOST',tag:'WASCTC/WASC-21',\
-//		   tag:'OWASP_TOP_10/A7',tag:'PCI/6.5.10',severity:'5',\
-//		   setvar:'tx.msg=%{rule.msg}',setvar:tx.anomaly_score=+%{tx.notice_anomaly_score},\
-//		   setvar:tx.protocol_violation_score=+%{tx.notice_anomaly_score},\
-//		   setvar:tx.%{rule.id}-PROTOCOL_VIOLATION/MISSING_HEADER-%{matched_var_name}=%{matched_var}"
+//		"id:'960008',phase:2,rev:'2.1.1',t:none,block,msg:'Request Missing a Host Header',\
+//		tag:'PROTOCOL_VIOLATION/MISSING_HEADER_HOST',tag:'WASCTC/WASC-21',\
+//		tag:'OWASP_TOP_10/A7',tag:'PCI/6.5.10',severity:'5',\
+//		setvar:'tx.msg=%{rule.msg}',setvar:tx.anomaly_score=+%{tx.notice_anomaly_score},\
+//		setvar:tx.protocol_violation_score=+%{tx.notice_anomaly_score},\
+//		setvar:tx.%{rule.id}-PROTOCOL_VIOLATION/MISSING_HEADER-%{matched_var_name}=%{matched_var}"
+//
+//	SecMarker END_HOST_CHECK
 //
-// SecMarker END_HOST_CHECK
 // ```
 func directiveSecMarker(options *DirectiveOptions) error {
+	if len(options.Opts) == 0 {
+		return errEmptyOptions
+	}
+
 	rule := corazawaf.NewRule()
 	rule.Raw_ = fmt.Sprintf("SecMarker %s", options.Opts)
 	rule.SecMark_ = options.Opts
@@ -94,7 +105,7 @@ func directiveSecMarker(options *DirectiveOptions) error {
 	rule.Line_ = options.Config.Get("parser_last_line", 0).(int)
 	rule.File_ = options.Config.Get("parser_config_file", "").(string)
 	if err := options.WAF.Rules.Add(rule); err != nil {
-		return newCompileRuleError(err, options.Opts)
+		return err
 	}
 	options.WAF.Logger.Debug("added secmark rule")
 	return nil
@@ -104,13 +115,17 @@ func directiveSecMarker(options *DirectiveOptions) error {
 // Syntax: SecAction "action1,action2,action3,..."
 // ----
 // This directive is commonly used to set variables and initialize persistent collections using the
-// initcol action. The syntax of the parameter is identical to that of the third parameter of SecRule.
+// `initcol` action. The syntax of the parameter is identical to that of the third parameter of `SecRule`.
 //
 // Example:
 // ```apache
 // SecAction "nolog,phase:1,initcol:RESOURCE=%{REQUEST_FILENAME}"
 // ```
 func directiveSecAction(options *DirectiveOptions) error {
+	if len(options.Opts) == 0 {
+		return errEmptyOptions
+	}
+
 	rule, err := ParseRule(RuleOptions{
 		WithOperator: false,
 		WAF:          options.WAF,
@@ -119,10 +134,10 @@ func directiveSecAction(options *DirectiveOptions) error {
 		Data:         options.Opts,
 	})
 	if err != nil {
-		return newCompileRuleError(err, options.Opts)
+		return err
 	}
 	if err := options.WAF.Rules.Add(rule); err != nil {
-		return newCompileRuleError(err, options.Opts)
+		return err
 	}
 	options.WAF.Logger.Debug("Added SecAction: %s", options.Opts)
 	return nil
@@ -134,16 +149,20 @@ func directiveSecAction(options *DirectiveOptions) error {
 // ---
 // Every rule must provide one or more variables along with the operator that should
 // be used to inspect them. If no actions are provided, the default list will be used.
-// (There is always a default list, even if one was not explicitly set with SecDefaultAction.)
+// (There is always a default list, even if one was not explicitly set with `SecDefaultAction`.)
 // If there are actions specified in a rule, they will be merged with the default list
 // to form the final actions that will be used. (The actions in the rule will overwrite
-// those in the default list.) Refer to SecDefaultAction for more information.
+// those in the default list.) Refer to `SecDefaultAction` for more information.
 //
 // Example:
 // ```apache
 // SecRule ARGS "@rx attack" "phase:1,log,deny,id:1"
 // ```
 func directiveSecRule(options *DirectiveOptions) error {
+	if len(options.Opts) == 0 {
+		return errEmptyOptions
+	}
+
 	ignoreErrors := options.Config.Get("ignore_rule_compilation_errors", false).(bool)
 	rule, err := ParseRule(RuleOptions{
 		WithOperator: true,
@@ -153,7 +172,7 @@ func directiveSecRule(options *DirectiveOptions) error {
 		Data:         options.Opts,
 	})
 	if err != nil && !ignoreErrors {
-		return newCompileRuleError(err, options.Opts)
+		return err
 	} else if err != nil && ignoreErrors {
 		options.WAF.Logger.Debug("Ignoring rule compilation error for rule %s: %s", options.Opts, err.Error())
 		return nil
@@ -175,12 +194,16 @@ func directiveSecRule(options *DirectiveOptions) error {
 // This directive is required if you plan to inspect HTML responses and implement
 // response blocking. Possible values are:
 // - On: buffer response bodies (but only if the response MIME type matches the list
-// configured with SecResponseBodyMimeType).
+// configured with `SecResponseBodyMimeType`).
 // - Off: do not buffer response bodies.
 func directiveSecResponseBodyAccess(options *DirectiveOptions) error {
+	if len(options.Opts) == 0 {
+		return errEmptyOptions
+	}
+
 	b, err := parseBoolean(strings.ToLower(options.Opts))
 	if err != nil {
-		return newDirectiveError(err, "SecResponseBodyAccess")
+		return err
 	}
 	options.WAF.ResponseBodyAccess = b
 	return nil
@@ -193,9 +216,16 @@ func directiveSecResponseBodyAccess(options *DirectiveOptions) error {
 // Anything over the limit will be rejected with status code 413 (Request Entity Too Large).
 // There is a hard limit of 1 GB.
 func directiveSecRequestBodyLimit(options *DirectiveOptions) error {
-	var err error
-	options.WAF.RequestBodyLimit, err = strconv.ParseInt(options.Opts, 10, 64)
-	return err
+	if len(options.Opts) == 0 {
+		return errEmptyOptions
+	}
+
+	limit, err := strconv.ParseInt(options.Opts, 10, 64)
+	if err != nil {
+		return err
+	}
+	options.WAF.RequestBodyLimit = limit
+	return nil
 }
 
 // Description: Configures whether request bodies will be buffered and processed by Coraza.
@@ -208,9 +238,13 @@ func directiveSecRequestBodyLimit(options *DirectiveOptions) error {
 // - On: buffer request bodies
 // - Off: do not buffer request bodies
 func directiveSecRequestBodyAccess(options *DirectiveOptions) error {
+	if len(options.Opts) == 0 {
+		return errEmptyOptions
+	}
+
 	b, err := parseBoolean(strings.ToLower(options.Opts))
 	if err != nil {
-		return newDirectiveError(err, "SecRequestBodyAccess")
+		return err
 	}
 	options.WAF.RequestBodyAccess = b
 	return nil
@@ -236,21 +270,37 @@ func directiveUnsupported(options *DirectiveOptions) error {
 }
 
 func directiveSecWebAppID(options *DirectiveOptions) error {
+	if len(options.Opts) == 0 {
+		return errEmptyOptions
+	}
+
 	options.WAF.WebAppID = options.Opts
 	return nil
 }
 
 func directiveSecTmpDir(options *DirectiveOptions) error {
+	if len(options.Opts) == 0 {
+		return errEmptyOptions
+	}
+
 	options.WAF.TmpDir = options.Opts
 	return nil
 }
 
 func directiveSecServerSignature(options *DirectiveOptions) error {
-	options.WAF.ServerSignature = options.Opts
+	if len(options.Opts) == 0 {
+		return errEmptyOptions
+	}
+
+	options.WAF.ServerSignature = utils.MaybeRemoveQuotes(options.Opts)
 	return nil
 }
 
 func directiveSecRuleRemoveByTag(options *DirectiveOptions) error {
+	if len(options.Opts) == 0 {
+		return errEmptyOptions
+	}
+
 	for _, r := range options.WAF.Rules.FindByTag(options.Opts) {
 		options.WAF.Rules.DeleteByID(r.ID_)
 	}
@@ -258,6 +308,10 @@ func directiveSecRuleRemoveByTag(options *DirectiveOptions) error {
 }
 
 func directiveSecRuleRemoveByMsg(options *DirectiveOptions) error {
+	if len(options.Opts) == 0 {
+		return errEmptyOptions
+	}
+
 	for _, r := range options.WAF.Rules.FindByMsg(options.Opts) {
 		options.WAF.Rules.DeleteByID(r.ID_)
 	}
@@ -265,23 +319,33 @@ func directiveSecRuleRemoveByMsg(options *DirectiveOptions) error {
 }
 
 func directiveSecRuleRemoveByID(options *DirectiveOptions) error {
-	id, _ := strconv.Atoi(options.Opts)
+	id, err := strconv.Atoi(options.Opts)
+	if err != nil {
+		return err
+	}
 	options.WAF.Rules.DeleteByID(id)
 	return nil
 }
 
 func directiveSecResponseBodyMimeTypesClear(options *DirectiveOptions) error {
+	if len(options.Opts) > 0 {
+		return errors.New("unexpected options")
+	}
 	options.WAF.ResponseBodyMimeTypes = nil
 	return nil
 }
 
 func directiveSecResponseBodyMimeType(options *DirectiveOptions) error {
+	if len(options.Opts) == 0 {
+		return errEmptyOptions
+	}
+
 	options.WAF.ResponseBodyMimeTypes = strings.Split(options.Opts, " ")
 	return nil
 }
 
 // Description: Controls what happens once a response body limit, configured with
-// SecResponseBodyLimit, is encountered.
+// `SecResponseBodyLimit`, is encountered.
 // Syntax: SecResponseBodyLimitAction Reject|ProcessPartial
 // ---
 // By default, Coraza will reject a response body that is longer than specified.
@@ -317,9 +381,16 @@ func directiveSecResponseBodyLimitAction(options *DirectiveOptions) error {
 // This setting will not affect the responses with MIME types that are not selected for
 // buffering. There is a hard limit of 1 GB.
 func directiveSecResponseBodyLimit(options *DirectiveOptions) error {
-	var err error
-	options.WAF.ResponseBodyLimit, err = strconv.ParseInt(options.Opts, 10, 64)
-	return err
+	if len(options.Opts) == 0 {
+		return errEmptyOptions
+	}
+
+	limit, err := strconv.ParseInt(options.Opts, 10, 64)
+	if err != nil {
+		return err
+	}
+	options.WAF.ResponseBodyLimit = limit
+	return nil
 }
 
 // Description: Controls what happens once a request body limit, configured with
@@ -345,16 +416,34 @@ func directiveSecRequestBodyLimitAction(options *DirectiveOptions) error {
 // Default: 131072 (128 KB)
 // Syntax: SecRequestBodyInMemoryLimit [LIMIT_IN_BYTES]
 // ---
-// When a multipart/form-data request is being processed, once the in-memory limit is reached,
+// When a `multipart/form-data` request is being processed, once the in-memory limit is reached,
 // the request body will start to be streamed into a temporary file on disk.
 func directiveSecRequestBodyInMemoryLimit(options *DirectiveOptions) error {
-	var err error
-	options.WAF.RequestBodyInMemoryLimit, err = strconv.ParseInt(options.Opts, 10, 64)
-	return err
+	if len(options.Opts) == 0 {
+		return errEmptyOptions
+	}
+
+	limit, err := strconv.ParseInt(options.Opts, 10, 64)
+	if err != nil {
+		return err
+	}
+	options.WAF.SetRequestBodyInMemoryLimit(limit)
+	return nil
 }
 
 func directiveSecRemoteRulesFailAction(options *DirectiveOptions) error {
-	options.WAF.AbortOnRemoteRulesFail = strings.ToLower(options.Opts) == "abort"
+	if len(options.Opts) == 0 {
+		return errEmptyOptions
+	}
+
+	switch strings.ToLower(options.Opts) {
+	case "abort":
+		options.WAF.AbortOnRemoteRulesFail = true
+	case "warn":
+		options.WAF.AbortOnRemoteRulesFail = false
+	default:
+		return errors.New("unknown option")
+	}
 	return nil
 }
 
@@ -367,6 +456,10 @@ func directiveSecConnWriteStateLimit(options *DirectiveOptions) error {
 }
 
 func directiveSecSensorID(options *DirectiveOptions) error {
+	if len(options.Opts) == 0 {
+		return errEmptyOptions
+	}
+
 	options.WAF.SensorID = options.Opts
 	return nil
 }
@@ -416,7 +509,7 @@ func directiveSecHashEngine(options *DirectiveOptions) error {
 // Default: phase:2,log,auditlog,pass
 // Syntax: SecDefaultAction "phase:2,log,auditlog,deny,status:403,tag:'SLA 24/7'"
 // ---
-// Every rule following a previous SecDefaultAction directive in the same configuration
+// Every rule following a previous `SecDefaultAction` directive in the same configuration
 // context will inherit its settings unless more specific actions are used.
 //
 // Rulesets like OWASP Core Ruleset uses this to define operation modes:
@@ -426,8 +519,13 @@ func directiveSecHashEngine(options *DirectiveOptions) error {
 // - You can set the default disruptive action to deny and each risky rule will interrupt
 // the connection.
 //
-// Important: Every SecDefaultAction directive must specify a disruptive action and a processing phase and cannot contain metadata actions.
+// Important: Every `SecDefaultAction` directive must specify a disruptive action and a processing
+// phase and cannot contain metadata actions.
 func directiveSecDefaultAction(options *DirectiveOptions) error {
+	if len(options.Opts) == 0 {
+		return errEmptyOptions
+	}
+
 	da, _ := options.Config.Get("rule_default_actions", []string{}).([]string)
 	da = append(da, options.Opts)
 	options.Config.Set("rule_default_actions", da)
@@ -474,8 +572,9 @@ func directiveSecCollectionTimeout(options *DirectiveOptions) error {
 // or for the directory.
 func directiveSecAuditLog(options *DirectiveOptions) error {
 	if len(options.Opts) == 0 {
-		return errors.New("syntax error: SecAuditLog /some/absolute/path.log")
+		return errEmptyOptions
 	}
+
 	options.Config.Set("auditlog_file", options.Opts)
 	if err := options.WAF.AuditLogWriter.Init(options.Config); err != nil {
 		return err
@@ -485,8 +584,9 @@ func directiveSecAuditLog(options *DirectiveOptions) error {
 
 func directiveSecAuditLogType(options *DirectiveOptions) error {
 	if len(options.Opts) == 0 {
-		return errors.New("syntax error: SecAuditLogType [concurrent/https/serial/...]")
+		return errEmptyOptions
 	}
+
 	writer, err := loggers.GetLogWriter(options.Opts)
 	if err != nil {
 		return err
@@ -504,13 +604,15 @@ func directiveSecAuditLogType(options *DirectiveOptions) error {
 // Default: Native
 func directiveSecAuditLogFormat(options *DirectiveOptions) error {
 	if len(options.Opts) == 0 {
-		return errors.New("syntax error: SecAuditLogFormat [json/native/...]")
+		return errEmptyOptions
 	}
+
 	formatter, err := loggers.GetLogFormatter(options.Opts)
 	if err != nil {
 		return err
 	}
 	options.Config.Set("auditlog_formatter", formatter)
+
 	if err := options.WAF.AuditLogWriter.Init(options.Config); err != nil {
 		return err
 	}
@@ -519,8 +621,9 @@ func directiveSecAuditLogFormat(options *DirectiveOptions) error {
 
 func directiveSecAuditLogDir(options *DirectiveOptions) error {
 	if len(options.Opts) == 0 {
-		return errors.New("syntax error: SecAuditLogDir /some/absolute/path")
+		return errEmptyOptions
 	}
+
 	options.Config.Set("auditlog_dir", options.Opts)
 	if err := options.WAF.AuditLogWriter.Init(options.Config); err != nil {
 		return err
@@ -542,8 +645,9 @@ func directiveSecAuditLogDir(options *DirectiveOptions) error {
 // ```
 func directiveSecAuditLogDirMode(options *DirectiveOptions) error {
 	if len(options.Opts) == 0 {
-		return errors.New("syntax error: SecAuditLogDirMode [0777/0700/...]")
+		return errEmptyOptions
 	}
+
 	auditLogDirMode, err := strconv.ParseInt(options.Opts, 8, 32)
 	if err != nil {
 		return err
@@ -556,7 +660,7 @@ func directiveSecAuditLogDirMode(options *DirectiveOptions) error {
 }
 
 // Description: Configures the mode (permissions) of any files created for concurrent
-// audit logs using an octal mode (as used in chmod). See SecAuditLogDirMode for
+// audit logs using an octal mode (as used in `chmod`). See `SecAuditLogDirMode` for
 // controlling the mode of created audit log directories.
 // Syntax: SecAuditLogFileMode octal_mode|"default"
 // Default: 0600
@@ -567,8 +671,9 @@ func directiveSecAuditLogDirMode(options *DirectiveOptions) error {
 // ```
 func directiveSecAuditLogFileMode(options *DirectiveOptions) error {
 	if len(options.Opts) == 0 {
-		return errors.New("syntax error: SecAuditLogFileMode [0777/0700/...]")
+		return errEmptyOptions
 	}
+
 	auditLogFileMode, err := strconv.ParseInt(options.Opts, 8, 32)
 	if err != nil {
 		return err
@@ -594,14 +699,18 @@ func directiveSecAuditLogFileMode(options *DirectiveOptions) error {
 // ```
 // This example would log all 5xx and 4xx level status codes,
 // except for 404s. Although you could achieve the same effect with a rule in phase 5,
-// SecAuditLogRelevantStatus is sometimes better, because it continues to work even when SecRuleEngine
-// is disabled.
+// `SecAuditLogRelevantStatus` is sometimes better, because it continues to work even when
+// `SecRuleEngine` is disabled.
 //
-// Note: Must have SecAuditEngine set to RelevantOnly. Additionally, the auditlog action
-// is present by default in rules, this will make the engine bypass the SecAuditLogRelevantStatus
+// Note: Must have `SecAuditEngine` set to `RelevantOnly`. Additionally, the auditlog action
+// is present by default in rules, this will make the engine bypass the `SecAuditLogRelevantStatus`
 // and send rule matches to the audit log regardless of status. You must specify noauditlog in the
-// rules manually or set it in SecDefaultAction.
+// rules manually or set it in `SecDefaultAction`.
 func directiveSecAuditLogRelevantStatus(options *DirectiveOptions) error {
+	if len(options.Opts) == 0 {
+		return errEmptyOptions
+	}
+
 	var err error
 	options.WAF.AuditLogRelevantStatus, err = regexp.Compile(options.Opts)
 	return err
@@ -612,7 +721,7 @@ func directiveSecAuditLogRelevantStatus(options *DirectiveOptions) error {
 // in the list then the equivalent part will be recorded. See below for the list of
 // all parts.
 // Syntax: SecAuditLogParts [PARTLETTERS]
-// Default: ABCFHZ Note
+// Default: ABCFHZ
 // ---
 // The format of the audit log format is documented in detail in the Audit Log Data
 // Format Documentation.
@@ -624,28 +733,32 @@ func directiveSecAuditLogRelevantStatus(options *DirectiveOptions) error {
 //
 // Available audit log parts:
 //
-// A: Audit log header (mandatory).
-// B: Request headers.
-// C: Request body (present only if the request body exists and Coraza is configured
-// to intercept it. This would require SecRequestBodyAccess to be set to on).
-// D: Reserved for intermediary response headers; not implemented yet.
-// E: Intermediary response body (present only if Coraza is configured to intercept
+// - A: Audit log header (mandatory).
+// - B: Request headers.
+// - C: Request body (present only if the request body exists and Coraza is configured
+// to intercept it. This would require `SecRequestBodyAccess` to be set to on).
+// - D: Reserved for intermediary response headers; not implemented yet.
+// - E: Intermediary response body (present only if Coraza is configured to intercept
 // response bodies, and if the audit log engine is configured to record it. Intercepting
-// response bodies requires SecResponseBodyAccess to be enabled). Intermediary response
+// response bodies requires `SecResponseBodyAccess` to be enabled). Intermediary response
 // body is the same as the actual response body unless Coraza intercepts the intermediary
 // response body, in which case the actual response body will contain the error message.
-// F: Final response headers.
-// G: Reserved for the actual response body; not implemented yet.
-// H: Audit log trailer.
-// I: This part is a replacement for part C. It will log the same data as C in all cases except when
-// multipart/form-data encoding in used. In this case, it will log a fake application/x-www-form-urlencoded
+// - F: Final response headers.
+// - G: Reserved for the actual response body; not implemented yet.
+// - H: Audit log trailer.
+// - I: This part is a replacement for part C. It will log the same data as C in all cases except when
+// `multipart/form-data` encoding in used. In this case, it will log a fake `application/x-www-form-urlencoded`
 // body that contains the information about parameters but not about the files. This is handy if
 // you don’t want to have (often large) files stored in your audit logs.
-// J: This part contains information about the files uploaded using multipart/form-data encoding.
-// K: This part contains a full list of every rule that matched (one per line) in the order they were
+// - J: This part contains information about the files uploaded using `multipart/form-data` encoding.
+// - K: This part contains a full list of every rule that matched (one per line) in the order they were
 // matched. The rules are fully qualified and will thus show inherited actions and default operators.
-// Z: Final boundary, signifies the end of the entry (mandatory).
+// - Z: Final boundary, signifies the end of the entry (mandatory).
 func directiveSecAuditLogParts(options *DirectiveOptions) error {
+	if len(options.Opts) == 0 {
+		return errEmptyOptions
+	}
+
 	options.WAF.AuditLogParts = types.AuditLogParts(options.Opts)
 	return nil
 }
@@ -654,20 +767,20 @@ func directiveSecAuditLogParts(options *DirectiveOptions) error {
 // Syntax: SecAuditEngine RelevantOnly
 // Default: Off
 // ---
-// The SecAuditEngine directive is used to configure the audit engine, which logs complete
+// The `SecAuditEngine` directive is used to configure the audit engine, which logs complete
 // transactions.
 //
 // The possible values for the audit log engine are as follows:
 //   - On: log all transactions
 //   - Off: do not log any transactions
 //   - RelevantOnly: only the log transactions that have triggered a warning or an error, or have
-//     a status code that is considered to be relevant (as determined by the SecAuditLogRelevantStatus
+//     a status code that is considered to be relevant (as determined by the `SecAuditLogRelevantStatus`
 //     directive)
 //
 // Note: If you need to change the audit log engine configuration on a per-transaction basis (e.g.,
-// in response to some transaction data), use the ctl action.
+// in response to some transaction data), use the `ctl` action.
 //
-// The following example demonstrates how SecAuditEngine is used:
+// The following example demonstrates how `SecAuditEngine` is used:
 // ```apache
 // SecAuditEngine RelevantOnly
 // SecAuditLog logs/audit/audit.log
@@ -677,13 +790,20 @@ func directiveSecAuditLogParts(options *DirectiveOptions) error {
 // SecAuditLogRelevantStatus ^(?:5|4(?!04))
 // ```
 func directiveSecAuditEngine(options *DirectiveOptions) error {
+	if len(options.Opts) == 0 {
+		return errEmptyOptions
+	}
+
 	au, err := types.ParseAuditEngineStatus(options.Opts)
 	options.WAF.AuditEngine = au
 	return err
 }
 
 func directiveSecDataDir(options *DirectiveOptions) error {
-	// TODO validations
+	if len(options.Opts) == 0 {
+		return errEmptyOptions
+	}
+
 	options.WAF.DataDir = options.Opts
 	return nil
 }
@@ -691,25 +811,40 @@ func directiveSecDataDir(options *DirectiveOptions) error {
 func directiveSecUploadKeepFiles(options *DirectiveOptions) error {
 	b, err := parseBoolean(options.Opts)
 	if err != nil {
-		return newDirectiveError(err, "SecUploadKeepFiles")
+		return err
 	}
 	options.WAF.UploadKeepFiles = b
 	return nil
 }
 
 func directiveSecUploadFileMode(options *DirectiveOptions) error {
+	if len(options.Opts) == 0 {
+		return errEmptyOptions
+	}
+
 	fm, err := strconv.ParseInt(options.Opts, 8, 32)
+	if err != nil {
+		return err
+	}
 	options.WAF.UploadFileMode = fs.FileMode(fm)
-	return err
+	return nil
 }
 
 func directiveSecUploadFileLimit(options *DirectiveOptions) error {
+	if len(options.Opts) == 0 {
+		return errEmptyOptions
+	}
+
 	var err error
 	options.WAF.UploadFileLimit, err = strconv.Atoi(options.Opts)
 	return err
 }
 
 func directiveSecUploadDir(options *DirectiveOptions) error {
+	if len(options.Opts) == 0 {
+		return errEmptyOptions
+	}
+
 	// TODO validations
 	options.WAF.UploadDir = options.Opts
 	return nil
@@ -719,7 +854,7 @@ func directiveSecUploadDir(options *DirectiveOptions) error {
 // buffering, excluding the size of any files being transported in the request.
 // This directive is useful to reduce susceptibility to DoS attacks when someone is
 // sending request bodies of very large sizes. Web applications that require file uploads
-// must configure SecRequestBodyLimit to a high value, but because large files are streamed
+// must configure `SecRequestBodyLimit` to a high value, but because large files are streamed
 // to disk, file uploads will not increase memory consumption. However, it’s still possible
 // for someone to take advantage of a large request body limit and send non-upload requests
 // with large body sizes. This directive eliminates that loophole.
@@ -730,6 +865,10 @@ func directiveSecUploadDir(options *DirectiveOptions) error {
 // should be able to reduce it down to 128 KB or lower. Anything over the limit will be
 // rejected with status code 413 (Request Entity Too Large). There is a hard limit of 1 GB.
 func directiveSecRequestBodyNoFilesLimit(options *DirectiveOptions) error {
+	if len(options.Opts) == 0 {
+		return errEmptyOptions
+	}
+
 	var err error
 	options.WAF.RequestBodyNoFilesLimit, err = strconv.ParseInt(options.Opts, 10, 64)
 	return err
@@ -741,26 +880,30 @@ func directiveSecRequestBodyNoFilesLimit(options *DirectiveOptions) error {
 // Logs will be written to this file. Make sure the process user has write access to the
 // directory.
 func directiveSecDebugLog(options *DirectiveOptions) error {
+	if len(options.Opts) == 0 {
+		return errEmptyOptions
+	}
+
 	return options.WAF.SetDebugLogPath(options.Opts)
 }
 
 // Description: Configures the verboseness of the debug log data.
-// Default: 2
+// Default: 3
 // Syntax: SecDebugLogLevel [LOG_LEVEL]
 // ---
 // Depending on the implementation, errors ranging from 1 to 2 might be directly
-// logged to the connector error log. For example, level 2 (error) logs will be
+// logged to the connector error log. For example, level 1 (error) logs will be
 // written to caddy server error logs.
 // The possible values for the debug log level are:
 //
-// 0: Fatal
-// 1: Panic
-// 2: Error
-// 3: Warning
-// 4: details of how transactions are handled
-// 5: log everything, including very detailed debugging information
+// - 0:   No logging (least verbose)
+// - 1:   Error
+// - 2:   Warn
+// - 3:   Info
+// - 4-8: Debug
+// - 9:   Trace (most verbose)
 //
-// All levels over 5 will be considered as 5.
+// Levels outside the 0-9 range will default to level 3 (Info)
 func directiveSecDebugLogLevel(options *DirectiveOptions) error {
 	lvl, err := strconv.Atoi(options.Opts)
 	if err != nil {
@@ -790,7 +933,7 @@ func directiveSecRuleUpdateTargetByID(options *DirectiveOptions) error {
 func directiveSecIgnoreRuleCompilationErrors(options *DirectiveOptions) error {
 	b, err := parseBoolean(options.Opts)
 	if err != nil {
-		return newDirectiveError(err, "SecIgnoreRuleCompilationErrors")
+		return err
 	}
 	if b {
 		options.WAF.Logger.Warn(`Coraza is running in Compatibility Mode (SecIgnoreRuleCompilationErrors On)
@@ -801,6 +944,10 @@ func directiveSecIgnoreRuleCompilationErrors(options *DirectiveOptions) error {
 }
 
 func directiveSecDataset(options *DirectiveOptions) error {
+	if len(options.Opts) == 0 {
+		return errEmptyOptions
+	}
+
 	name, d, ok := strings.Cut(options.Opts, " ")
 	if !ok {
 		return errors.New("syntax error: SecDataset name `\n...\n`")
@@ -821,14 +968,6 @@ func directiveSecDataset(options *DirectiveOptions) error {
 	return nil
 }
 
-func newCompileRuleError(err error, opts string) error {
-	return fmt.Errorf("failed to compile rule (%s): %s", err, opts)
-}
-
-func newDirectiveError(err error, directive string) error {
-	return fmt.Errorf("syntax error for directive %s: %s", directive, err)
-}
-
 func parseBoolean(data string) (bool, error) {
 	data = strings.ToLower(data)
 	switch data {
diff --git a/internal/seclang/parser.go b/internal/seclang/parser.go
index e521ffd..3a56213 100644
--- a/internal/seclang/parser.go
+++ b/internal/seclang/parser.go
@@ -140,6 +140,7 @@ func (p *Parser) evaluateLine(data string) error {
 	}
 	// first we get the directive
 	dir, opts, _ := strings.Cut(data, " ")
+
 	p.options.WAF.Logger.Debug("parsing directive %q", data)
 	directive := strings.ToLower(dir)
 
@@ -152,14 +153,14 @@ func (p *Parser) evaluateLine(data string) error {
 		// note a user might still include another file that includes the original file
 		// generating a DDOS attack
 		if p.includeCount >= maxIncludeRecursion {
-			return fmt.Errorf("cannot include more than %d files", maxIncludeRecursion)
+			return p.log(fmt.Sprintf("cannot include more than %d files", maxIncludeRecursion))
 		}
 		p.includeCount++
 		return p.FromFile(opts)
 	}
 	d, ok := directivesMap[directive]
 	if !ok || d == nil {
-		return p.log("Unknown directive " + directive)
+		return p.log(fmt.Sprintf("unknown directive %q", directive))
 	}
 
 	p.options.Opts = opts
@@ -173,7 +174,11 @@ func (p *Parser) evaluateLine(data string) error {
 	}
 	p.options.Config.Set("working_dir", wd)
 
-	return d(p.options)
+	if err := d(p.options); err != nil {
+		return fmt.Errorf("failed to compile the directive %q: %w", directive, err)
+	}
+
+	return nil
 }
 
 func (p *Parser) log(msg string) error {
diff --git a/loggers/debug.go b/loggers/debug.go
index 3f028bb..b316200 100644
--- a/loggers/debug.go
+++ b/loggers/debug.go
@@ -9,12 +9,12 @@ import (
 
 // DebugLogger is used to log SecDebugLog messages
 type DebugLogger interface {
-	// Info logs an info message
-	Info(message string, args ...interface{})
-	// Warn logs a warning message
-	Warn(message string, args ...interface{})
 	// Error logs an error message
 	Error(message string, args ...interface{})
+	// Warn logs a warning message
+	Warn(message string, args ...interface{})
+	// Info logs an info message
+	Info(message string, args ...interface{})
 	// Debug logs a debug message
 	Debug(message string, args ...interface{})
 	// Trace logs a trace message
@@ -31,31 +31,37 @@ type LogLevel int
 
 const (
 	// LogLevelUnknown is a default value for unknown log level
-	LogLevelUnknown LogLevel = iota
-	// LogLevelInfo is the lowest level of logging
-	LogLevelInfo
+	LogLevelUnknown LogLevel = iota - 1
+	// LogLevelNoLog is the lowest level of logging, no logs are generated
+	LogLevelNoLog
+	// LogLevelError is the level of logging only for errors
+	LogLevelError
 	// LogLevelWarn is the level of logging for warnings
 	LogLevelWarn
-	// LogLevelError is the level of logging for errors
-	LogLevelError
+	// LogLevelInfo is the lowest of logging for informational messages
+	LogLevelInfo
 	// LogLevelDebug is the level of logging for debug messages
 	LogLevelDebug
+	// ModSecurity compatibility, levels 4-8 will be Debug level
+	_ = iota + 2
 	// LogLevelTrace is the highest level of logging
 	LogLevelTrace
 )
 
 // String returns the string representation of the log level
 func (level LogLevel) String() string {
-	switch level {
-	case LogLevelInfo:
-		return "INFO"
-	case LogLevelWarn:
-		return "WARN"
-	case LogLevelError:
+	switch {
+	case level == LogLevelNoLog:
+		return "NOLOG"
+	case level == LogLevelError:
 		return "ERROR"
-	case LogLevelDebug:
+	case level == LogLevelWarn:
+		return "WARN"
+	case level == LogLevelInfo:
+		return "INFO"
+	case level >= LogLevelDebug && level < LogLevelTrace:
 		return "DEBUG"
-	case LogLevelTrace:
+	case level == LogLevelTrace:
 		return "TRACE"
 	}
 	return "UNKNOWN"
@@ -63,5 +69,5 @@ func (level LogLevel) String() string {
 
 // Invalid returns true if the log level is invalid
 func (level LogLevel) Invalid() bool {
-	return level < LogLevelInfo || level > LogLevelTrace
+	return level < LogLevelNoLog || level > LogLevelTrace
 }
diff --git a/loggers/serial_writer.go b/loggers/serial_writer.go
index a968d83..4cbeac4 100644
--- a/loggers/serial_writer.go
+++ b/loggers/serial_writer.go
@@ -45,6 +45,10 @@ func (sl *serialWriter) Init(c types.Config) error {
 }
 
 func (sl *serialWriter) Write(al *AuditLog) error {
+	if sl.formatter == nil {
+		return nil
+	}
+
 	bts, err := sl.formatter(al)
 	if err != nil {
 		return err
diff --git a/macro/macro.go b/macro/macro.go
index f328699..989ee95 100644
--- a/macro/macro.go
+++ b/macro/macro.go
@@ -29,7 +29,7 @@ func NewMacro(data string) (Macro, error) {
 
 type macroToken struct {
 	text     string
-	variable *variables.RuleVariable
+	variable variables.RuleVariable
 	key      string
 }
 
@@ -59,23 +59,19 @@ func (m *macro) Expand(tx rules.TransactionState) string {
 }
 
 func expandToken(tx rules.TransactionState, token macroToken) string {
-	if token.variable == nil {
+	if token.variable == variables.Unknown {
 		return token.text
 	}
-	switch col := tx.Collection(*token.variable).(type) {
-	case *collection.Map:
+	switch col := tx.Collection(token.variable).(type) {
+	case collection.Keyed:
 		if c := col.Get(token.key); len(c) > 0 {
 			return c[0]
 		}
-	case *collection.Simple:
-		return col.String()
-	case *collection.Proxy:
-		if c := col.Get(token.key); len(c) > 0 {
-			return c[0]
-		}
-	case *collection.TranslationProxy:
-		if c := col.Get(0); len(c) > 0 {
-			return c
+	case collection.Single:
+		return col.Get()
+	default:
+		if c := col.FindAll(); len(c) > 0 {
+			return c[0].Value()
 		}
 	}
 
@@ -100,7 +96,7 @@ func (m *macro) compile(input string) error {
 				// we add the text token
 				m.tokens = append(m.tokens, macroToken{
 					text:     currentToken.String(),
-					variable: nil,
+					variable: variables.Unknown,
 					key:      "",
 				})
 			}
@@ -121,7 +117,7 @@ func (m *macro) compile(input string) error {
 				// we add the variable token
 				m.tokens = append(m.tokens, macroToken{
 					text:     currentToken.String(),
-					variable: &v,
+					variable: v,
 					key:      strings.ToLower(key),
 				})
 				currentToken.Reset()
@@ -137,7 +133,7 @@ func (m *macro) compile(input string) error {
 	if currentToken.Len() > 0 {
 		m.tokens = append(m.tokens, macroToken{
 			text:     currentToken.String(),
-			variable: nil,
+			variable: variables.Unknown,
 			key:      "",
 		})
 	}
diff --git a/magefile.go b/magefile.go
index 18e63af..1b597a1 100644
--- a/magefile.go
+++ b/magefile.go
@@ -57,7 +57,7 @@ func Lint() error {
 		return err
 	}
 
-	if sh.Run("git", "diff", "--exit-code") != nil {
+	if sh.Run("git", "diff", "--exit-code", "--", "'*.gen.go'") != nil {
 		return errUpdateGeneratedFiles
 	}
 
diff --git a/rules/transaction.go b/rules/transaction.go
index 8e797c3..e6fe0c3 100644
--- a/rules/transaction.go
+++ b/rules/transaction.go
@@ -46,98 +46,93 @@ type TransactionState interface {
 // TransactionVariables has pointers to all the variables of the transaction
 type TransactionVariables interface {
 	// Simple Variables
-	UserID() *collection.Simple
-	UrlencodedError() *collection.Simple
-	ResponseContentType() *collection.Simple
-	UniqueID() *collection.Simple
-	ArgsCombinedSize() *collection.SizeProxy
-	AuthType() *collection.Simple
-	FilesCombinedSize() *collection.Simple
-	FullRequest() *collection.Simple
-	FullRequestLength() *collection.Simple
-	InboundDataError() *collection.Simple
-	MatchedVar() *collection.Simple
-	MatchedVarName() *collection.Simple
-	MultipartBoundaryQuoted() *collection.Simple
-	MultipartBoundaryWhitespace() *collection.Simple
-	MultipartCrlfLfLines() *collection.Simple
-	MultipartDataAfter() *collection.Simple
-	MultipartDataBefore() *collection.Simple
-	MultipartFileLimitExceeded() *collection.Simple
-	MultipartPartHeaders() *collection.Map
-	MultipartHeaderFolding() *collection.Simple
-	MultipartInvalidHeaderFolding() *collection.Simple
-	MultipartInvalidPart() *collection.Simple
-	MultipartInvalidQuoting() *collection.Simple
-	MultipartLfLine() *collection.Simple
-	MultipartMissingSemicolon() *collection.Simple
-	MultipartStrictError() *collection.Simple
-	MultipartUnmatchedBoundary() *collection.Simple
-	OutboundDataError() *collection.Simple
-	PathInfo() *collection.Simple
-	QueryString() *collection.Simple
-	RemoteAddr() *collection.Simple
-	RemoteHost() *collection.Simple
-	RemotePort() *collection.Simple
-	RequestBodyError() *collection.Simple
-	RequestBodyErrorMsg() *collection.Simple
-	RequestBodyProcessorError() *collection.Simple
-	RequestBodyProcessorErrorMsg() *collection.Simple
-	RequestBodyProcessor() *collection.Simple
-	RequestBasename() *collection.Simple
-	RequestBody() *collection.Simple
-	RequestBodyLength() *collection.Simple
-	RequestFilename() *collection.Simple
-	RequestLine() *collection.Simple
-	RequestMethod() *collection.Simple
-	RequestProtocol() *collection.Simple
-	RequestURI() *collection.Simple
-	RequestURIRaw() *collection.Simple
-	ResponseBody() *collection.Simple
-	ResponseContentLength() *collection.Simple
-	ResponseProtocol() *collection.Simple
-	ResponseStatus() *collection.Simple
-	ServerAddr() *collection.Simple
-	ServerName() *collection.Simple
-	ServerPort() *collection.Simple
-	SessionID() *collection.Simple
-	HighestSeverity() *collection.Simple
-	StatusLine() *collection.Simple
-	InboundErrorData() *collection.Simple
-	// Custom
-	Env() *collection.Map
-	TX() *collection.Map
-	Rule() *collection.Map
-	Duration() *collection.Simple
-	// Proxy Variables
-	Args() *collection.Proxy
-	// Maps Variables
-	ArgsGet() *collection.Map
-	ArgsPost() *collection.Map
-	ArgsPath() *collection.Map
-	FilesTmpNames() *collection.Map
-	Geo() *collection.Map
-	Files() *collection.Map
-	RequestCookies() *collection.Map
-	RequestHeaders() *collection.Map
-	ResponseHeaders() *collection.Map
-	MultipartName() *collection.Map
-	MatchedVarsNames() *collection.Map
-	MultipartFilename() *collection.Map
-	MatchedVars() *collection.Map
-	FilesSizes() *collection.Map
-	FilesNames() *collection.Map
-	FilesTmpContent() *collection.Map
-	ResponseHeadersNames() *collection.Map
-	RequestHeadersNames() *collection.Map
-	RequestCookiesNames() *collection.Map
-	XML() *collection.Map
-	RequestXML() *collection.Map
-	ResponseXML() *collection.Map
-	// Persistent variables
-	IP() *collection.Map
-	// Translation Proxy Variables
-	ArgsNames() *collection.TranslationProxy
-	ArgsGetNames() *collection.TranslationProxy
-	ArgsPostNames() *collection.TranslationProxy
+	UserID() collection.Single
+	UrlencodedError() collection.Single
+	ResponseContentType() collection.Single
+	UniqueID() collection.Single
+	ArgsCombinedSize() collection.Collection
+	AuthType() collection.Single
+	FilesCombinedSize() collection.Single
+	FullRequest() collection.Single
+	FullRequestLength() collection.Single
+	InboundDataError() collection.Single
+	MatchedVar() collection.Single
+	MatchedVarName() collection.Single
+	MultipartBoundaryQuoted() collection.Single
+	MultipartBoundaryWhitespace() collection.Single
+	MultipartCrlfLfLines() collection.Single
+	MultipartDataAfter() collection.Single
+	MultipartDataBefore() collection.Single
+	MultipartFileLimitExceeded() collection.Single
+	MultipartPartHeaders() collection.Map
+	MultipartHeaderFolding() collection.Single
+	MultipartInvalidHeaderFolding() collection.Single
+	MultipartInvalidPart() collection.Single
+	MultipartInvalidQuoting() collection.Single
+	MultipartLfLine() collection.Single
+	MultipartMissingSemicolon() collection.Single
+	MultipartStrictError() collection.Single
+	MultipartUnmatchedBoundary() collection.Single
+	OutboundDataError() collection.Single
+	PathInfo() collection.Single
+	QueryString() collection.Single
+	RemoteAddr() collection.Single
+	RemoteHost() collection.Single
+	RemotePort() collection.Single
+	RequestBodyError() collection.Single
+	RequestBodyErrorMsg() collection.Single
+	RequestBodyProcessorError() collection.Single
+	RequestBodyProcessorErrorMsg() collection.Single
+	RequestBodyProcessor() collection.Single
+	RequestBasename() collection.Single
+	RequestBody() collection.Single
+	RequestBodyLength() collection.Single
+	RequestFilename() collection.Single
+	RequestLine() collection.Single
+	RequestMethod() collection.Single
+	RequestProtocol() collection.Single
+	RequestURI() collection.Single
+	RequestURIRaw() collection.Single
+	ResponseBody() collection.Single
+	ResponseContentLength() collection.Single
+	ResponseProtocol() collection.Single
+	ResponseStatus() collection.Single
+	ServerAddr() collection.Single
+	ServerName() collection.Single
+	ServerPort() collection.Single
+	SessionID() collection.Single
+	HighestSeverity() collection.Single
+	StatusLine() collection.Single
+	InboundErrorData() collection.Single
+	Env() collection.Map
+	TX() collection.Map
+	Rule() collection.Map
+	Duration() collection.Single
+	Args() collection.Keyed
+	ArgsGet() collection.Map
+	ArgsPost() collection.Map
+	ArgsPath() collection.Map
+	FilesTmpNames() collection.Map
+	Geo() collection.Map
+	Files() collection.Map
+	RequestCookies() collection.Map
+	RequestHeaders() collection.Map
+	ResponseHeaders() collection.Map
+	MultipartName() collection.Map
+	MatchedVarsNames() collection.Collection
+	MultipartFilename() collection.Map
+	MatchedVars() collection.Map
+	FilesSizes() collection.Map
+	FilesNames() collection.Map
+	FilesTmpContent() collection.Map
+	ResponseHeadersNames() collection.Collection
+	RequestHeadersNames() collection.Collection
+	RequestCookiesNames() collection.Collection
+	XML() collection.Map
+	RequestXML() collection.Map
+	ResponseXML() collection.Map
+	IP() collection.Map
+	ArgsNames() collection.Collection
+	ArgsGetNames() collection.Collection
+	ArgsPostNames() collection.Collection
 }
diff --git a/types/anchoredvar.go b/types/anchoredvar.go
deleted file mode 100644
index 7f0db39..0000000
--- a/types/anchoredvar.go
+++ /dev/null
@@ -1,12 +0,0 @@
-// Copyright 2022 Juan Pablo Tosso and the OWASP Coraza contributors
-// SPDX-License-Identifier: Apache-2.0
-
-package types
-
-// AnchoredVar stores the case preserved Original name and value
-// of the variable
-type AnchoredVar struct {
-	// Key sensitive name
-	Name  string
-	Value string
-}
diff --git a/types/transaction.go b/types/transaction.go
index ec1640c..637e217 100644
--- a/types/transaction.go
+++ b/types/transaction.go
@@ -33,7 +33,6 @@ type Transaction interface {
 	// ProcessConnection should be called at very beginning of a request process, it is
 	// expected to be executed prior to the virtual host resolution, when the
 	// connection arrives on the server.
-	// Important: Remember to check for a possible intervention.
 	ProcessConnection(client string, cPort int, server string, sPort int)
 
 	// ProcessURI Performs the analysis on the URI and all the query string variables.
@@ -48,6 +47,13 @@ type Transaction interface {
 	// note: This function won't add GET arguments, they must be added with AddArgument
 	ProcessURI(uri string, method string, httpVersion string)
 
+	// SetServerName allows to set server name details.
+	// The API consumer is in charge of retrieving the value (e.g. from the host header)
+	// before providing it to this method.
+	// In order to be able to check SERVER_NAME variable since phase 1, it is expected
+	// to execute SetServerName before calling ProcessRequestHeaders.
+	SetServerName(serverName string)
+
 	// AddRequestHeader Adds a request header
 	//
 	// With this method it is possible to feed Coraza with a request header.
diff --git a/types/waf.go b/types/waf.go
index 1b3c37f..98e7ebe 100644
--- a/types/waf.go
+++ b/types/waf.go
@@ -80,7 +80,7 @@ func ParseRuleEngineStatus(re string) (RuleEngineStatus, error) {
 	case "off":
 		return RuleEngineOff, nil
 	}
-	return -1, fmt.Errorf("invalid rule engine status: %s", re)
+	return -1, fmt.Errorf("invalid rule engine status: %q", re)
 }
 
 // String returns the string representation of the
@@ -88,11 +88,11 @@ func ParseRuleEngineStatus(re string) (RuleEngineStatus, error) {
 func (re RuleEngineStatus) String() string {
 	switch re {
 	case RuleEngineOn:
-		return "on"
+		return "On"
 	case RuleEngineDetectionOnly:
 		return "DetectionOnly"
 	case RuleEngineOff:
-		return "off"
+		return "Off"
 	}
 	return "unknown"
 }
diff --git a/waf.go b/waf.go
index a4e666a..ce39840 100644
--- a/waf.go
+++ b/waf.go
@@ -4,7 +4,6 @@
 package coraza
 
 import (
-	"errors"
 	"fmt"
 
 	"github.com/corazawaf/coraza/v3/internal/corazawaf"
@@ -77,39 +76,30 @@ func NewWAF(config WAFConfig) (WAF, error) {
 		waf.RequestBodyAccess = true
 	}
 
-	if c.requestBodyLimit != unsetLimit {
-		if c.requestBodyLimit <= 0 {
-			return nil, errors.New("request body limit should be bigger than 0")
-		}
-
-		if c.requestBodyLimit < c.requestBodyInMemoryLimit {
-			return nil, errors.New("request body limit should be at least the memory limit")
-		}
-		waf.RequestBodyLimit = int64(c.requestBodyLimit)
+	if c.requestBodyLimit != nil {
+		waf.RequestBodyLimit = int64(*c.requestBodyLimit)
 	}
 
-	if c.requestBodyInMemoryLimit != unsetLimit {
-		if c.requestBodyInMemoryLimit <= 0 {
-			return nil, errors.New("request body memory limit should be bigger than 0")
-		}
-		waf.RequestBodyInMemoryLimit = int64(c.requestBodyInMemoryLimit)
+	if c.requestBodyInMemoryLimit != nil {
+		waf.SetRequestBodyInMemoryLimit(int64(*c.requestBodyInMemoryLimit))
 	}
 
 	if c.responseBodyAccess {
 		waf.ResponseBodyAccess = true
 	}
 
-	if c.responseBodyLimit != unsetLimit {
-		if c.responseBodyLimit <= 0 {
-			return nil, errors.New("response body limit should be bigger than 0")
-		}
-		waf.ResponseBodyLimit = int64(c.responseBodyLimit)
+	if c.responseBodyLimit != nil {
+		waf.ResponseBodyLimit = int64(*c.responseBodyLimit)
 	}
 
 	if c.errorCallback != nil {
 		waf.ErrorLogCb = c.errorCallback
 	}
 
+	if err := waf.Validate(); err != nil {
+		return nil, err
+	}
+
 	return wafWrapper{waf: waf}, nil
 }
 


Test patch:
diff --git a/actions/ctl_test.go b/actions/ctl_test.go
index d5bdb1c..1675420 100644
--- a/actions/ctl_test.go
+++ b/actions/ctl_test.go
@@ -24,7 +24,7 @@ func TestCtl(t *testing.T) {
 			t.Errorf("failed to init requestBodyProcessor %s", bp)
 		}
 		ctlf.Evaluate(r, tx)
-		if tx.Variables().RequestBodyProcessor().String() != bp {
+		if tx.Variables().RequestBodyProcessor().Get() != bp {
 			t.Error("failed to set RequestBodyProcessor " + bp)
 		}
 	}
diff --git a/bodyprocessors/urlencoded_test.go b/bodyprocessors/urlencoded_test.go
index 4884447..d480a2b 100644
--- a/bodyprocessors/urlencoded_test.go
+++ b/bodyprocessors/urlencoded_test.go
@@ -4,6 +4,7 @@
 package bodyprocessors_test
 
 import (
+	"strconv"
 	"strings"
 	"testing"
 
@@ -31,11 +32,11 @@ func TestURLEncode(t *testing.T) {
 	if err := bp.ProcessRequest(strings.NewReader(body), v, bodyprocessors.Options{}); err != nil {
 		t.Error(err)
 	}
-	if v.RequestBody().String() != body {
-		t.Errorf("Expected %s, got %s", body, v.RequestBody().String())
+	if v.RequestBody().Get() != body {
+		t.Errorf("Expected %s, got %s", body, v.RequestBody().Get())
 	}
-	if v.RequestBodyLength().Int() != len(body) {
-		t.Errorf("Expected %d, got %s", len(body), v.RequestBodyLength().String())
+	if rbl, _ := strconv.Atoi(v.RequestBodyLength().Get()); rbl != len(body) {
+		t.Errorf("Expected %d, got %s", len(body), v.RequestBodyLength().Get())
 	}
 	for k, val := range m {
 		if v.ArgsPost().Get(k)[0] != val {
diff --git a/bodyprocessors/xml_test.go b/bodyprocessors/xml_test.go
index 88db96f..3d714cb 100644
--- a/bodyprocessors/xml_test.go
+++ b/bodyprocessors/xml_test.go
@@ -50,3 +50,24 @@ func TestXMLAttribures(t *testing.T) {
 		}
 	}
 }
+
+func TestXMLPayloadFlexibility(t *testing.T) {
+	xmldoc := `<note>
+			<to>Tove</to>
+			<from>Jani</from>
+			<heading>Reminder</heading>
+			<body>Don't forget me this weekend!
+		</note>`
+	_, contents, err := readXML(bytes.NewReader([]byte(xmldoc)))
+	if err != nil {
+		t.Error(err)
+	}
+	for _, content := range []string{"Tove", "Jani", "Reminder", "Don't forget me this weekend!"} {
+		if !strings.InSlice(content, contents) {
+			t.Errorf("Expected content %s, got %v", content, contents)
+		}
+	}
+	if len(contents) != 4 {
+		t.Errorf("Expected 4 contents, got %d", len(contents))
+	}
+}
diff --git a/collection/collection_proxy_test.go b/collection/collection_proxy_test.go
deleted file mode 100644
index ae07cd2..0000000
--- a/collection/collection_proxy_test.go
+++ /dev/null
@@ -1,64 +0,0 @@
-// Copyright 2022 Juan Pablo Tosso
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//	http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-package collection
-
-import (
-	"regexp"
-	"sort"
-	"testing"
-
-	"github.com/corazawaf/coraza/v3/types/variables"
-)
-
-func TestCollectionProxy(t *testing.T) {
-	c1 := NewMap(variables.ArgsPost)
-	c2 := NewMap(variables.ArgsGet)
-	proxy := NewProxy(variables.Args, c1, c2)
-
-	c1.Set("key1", []string{"value1"})
-	c1.Set("key2", []string{"value2"})
-	c2.Set("key3", []string{"value3"})
-
-	p := proxy.FindAll()
-	if len(p) != 3 {
-		t.Error("Error finding all")
-	} else {
-		p := proxy.FindAll()
-		m := false
-		for _, v := range p {
-			if v.Value() == "value1" {
-				m = true
-				break
-			}
-		}
-		if !m {
-			t.Error("Error finding all")
-		}
-	}
-	var f []string
-	for _, r := range p {
-		f = append(f, r.Value())
-	}
-	sort.Strings(f)
-	if f[0] != "value1" || f[1] != "value2" || f[2] != "value3" {
-		t.Error("Error finding all")
-	}
-
-	if len(proxy.FindString("key3")) == 0 {
-		t.Error("Error finding string")
-	}
-	if len(proxy.FindRegex(regexp.MustCompile("k.*"))) != 3 {
-		t.Error("Error finding regex")
-	}
-}
diff --git a/collection/collection_size_proxy_test.go b/collection/collection_size_proxy_test.go
deleted file mode 100644
index 65c54cc..0000000
--- a/collection/collection_size_proxy_test.go
+++ /dev/null
@@ -1,34 +0,0 @@
-// Copyright 2022 Juan Pablo Tosso
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//	http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-package collection
-
-import (
-	"testing"
-
-	"github.com/corazawaf/coraza/v3/types/variables"
-)
-
-func TestCollectionSizeProxy(t *testing.T) {
-	c1 := NewMap(variables.ArgsPost)
-	c2 := NewMap(variables.ArgsGet)
-	proxy := NewCollectionSizeProxy(variables.Args, c1, c2)
-
-	c1.Set("key1", []string{"value1", "value2"})
-	c1.Set("key2", []string{"value2"})
-	c2.Set("key3", []string{"value3"})
-	if proxy.Size() != 24 {
-		t.Errorf("Error finding size for size proxy, got %d", proxy.Size())
-	}
-
-}
diff --git a/collection/collection_translation_proxy_test.go b/collection/collection_translation_proxy_test.go
deleted file mode 100644
index 2cab5a6..0000000
--- a/collection/collection_translation_proxy_test.go
+++ /dev/null
@@ -1,47 +0,0 @@
-// Copyright 2022 Juan Pablo Tosso
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//	http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-package collection
-
-import (
-	"regexp"
-	"testing"
-
-	"github.com/corazawaf/coraza/v3/types/variables"
-)
-
-func TestCollectionTranslationProxy(t *testing.T) {
-	c1 := NewMap(variables.ArgsPost)
-	c2 := NewMap(variables.ArgsGet)
-	proxy := NewTranslationProxy(variables.ArgsNames, c1, c2)
-
-	c1.SetCS("key1", "key1", []string{"value1"})
-	c1.Set("key2", []string{"value2"})
-	c2.SetCS("key3", "Key3", []string{"value3"})
-
-	if len(proxy.FindAll()) != 3 {
-		t.Error("Error finding all")
-	}
-	if len(proxy.FindString("key3")) == 0 {
-		t.Error("Error finding string")
-	}
-	if proxy.FindString("key1")[0].Value() != "key1" {
-		t.Error("Error value findstring key1")
-	}
-	if proxy.FindString("key3")[0].Value() != "key3" {
-		t.Error("Error value findstring key3")
-	}
-	if len(proxy.FindRegex(regexp.MustCompile("k.*"))) != 3 {
-		t.Error("Error finding regex")
-	}
-}
diff --git a/examples/http-server/main_test.go b/examples/http-server/main_test.go
index abb8c21..41eafeb 100644
--- a/examples/http-server/main_test.go
+++ b/examples/http-server/main_test.go
@@ -79,7 +79,7 @@ func TestHttpServer(t *testing.T) {
 		{
 			"positive for response body limit reject",
 			"/",
-			403,
+			413,
 			map[string]string{
 				"DIRECTIVES_FILE": "./testdata/response-body-limits-reject.conf",
 				"RESPONSE_BODY":   "response body beyond the limit",
diff --git a/examples/http-server/testdata/request-body-limits-processpartial.conf b/examples/http-server/testdata/request-body-limits-processpartial.conf
index 8d1c897..b359eb9 100644
--- a/examples/http-server/testdata/request-body-limits-processpartial.conf
+++ b/examples/http-server/testdata/request-body-limits-processpartial.conf
@@ -1,4 +1,4 @@
-SecDebugLogLevel 5
+SecDebugLogLevel 9
 SecDebugLog /dev/stdout
 SecRequestBodyAccess On
 SecRequestBodyInMemoryLimit 5
diff --git a/examples/http-server/testdata/response-body-limits-reject.conf b/examples/http-server/testdata/response-body-limits-reject.conf
index 6af2f57..506017d 100644
--- a/examples/http-server/testdata/response-body-limits-reject.conf
+++ b/examples/http-server/testdata/response-body-limits-reject.conf
@@ -1,4 +1,4 @@
-SecDebugLogLevel 5
+SecDebugLogLevel 9
 SecDebugLog /dev/stdout
 SecResponseBodyAccess On
 SecResponseBodyMimeType text/plain
diff --git a/examples/http-server/testdata/response-body.conf b/examples/http-server/testdata/response-body.conf
index 7dfefe2..073bcec 100644
--- a/examples/http-server/testdata/response-body.conf
+++ b/examples/http-server/testdata/response-body.conf
@@ -1,4 +1,4 @@
-SecDebugLogLevel 5
+SecDebugLogLevel 9
 SecDebugLog /dev/stdout
 SecResponseBodyAccess On
 SecResponseBodyMimeType text/plain
diff --git a/examples/http-server/testdata/response-headers.conf b/examples/http-server/testdata/response-headers.conf
index f74e2c2..b894845 100644
--- a/examples/http-server/testdata/response-headers.conf
+++ b/examples/http-server/testdata/response-headers.conf
@@ -1,3 +1,3 @@
-SecDebugLogLevel 5
+SecDebugLogLevel 9
 SecDebugLog /dev/stdout
 SecRule RESPONSE_HEADERS:Foo "@pm bar" "id:199,phase:3,deny,t:lowercase,deny, status:403,msg:'Invalid response header',log,auditlog"
diff --git a/http/middleware_test.go b/http/middleware_test.go
index b8c88bd..6f0c81d 100644
--- a/http/middleware_test.go
+++ b/http/middleware_test.go
@@ -30,12 +30,12 @@ import (
 
 func TestProcessRequest(t *testing.T) {
 	req, _ := http.NewRequest("POST", "https://www.coraza.io/test", strings.NewReader("test=456"))
-	waf := corazawaf.NewWAF()
-	tx := waf.NewTransaction()
+	waf, _ := coraza.NewWAF(coraza.NewWAFConfig())
+	tx := waf.NewTransaction().(*corazawaf.Transaction)
 	if _, err := processRequest(tx, req); err != nil {
 		t.Fatal(err)
 	}
-	if tx.Variables().RequestMethod().String() != "POST" {
+	if tx.Variables().RequestMethod().Get() != "POST" {
 		t.Fatal("failed to set request from request object")
 	}
 	if err := tx.Close(); err != nil {
@@ -45,13 +45,13 @@ func TestProcessRequest(t *testing.T) {
 
 func TestProcessRequestEngineOff(t *testing.T) {
 	req, _ := http.NewRequest("POST", "https://www.coraza.io/test", strings.NewReader("test=456"))
-	waf := corazawaf.NewWAF()
-	waf.RuleEngine = types.RuleEngineOff
-	tx := waf.NewTransaction()
+	// TODO(jcchavezs): Shall we make RuleEngine a first class method in WAF config?
+	waf, _ := coraza.NewWAF(coraza.NewWAFConfig().WithDirectives("SecRuleEngine OFF"))
+	tx := waf.NewTransaction().(*corazawaf.Transaction)
 	if _, err := processRequest(tx, req); err != nil {
 		t.Fatal(err)
 	}
-	if tx.Variables().RequestMethod().String() != "POST" {
+	if tx.Variables().RequestMethod().Get() != "POST" {
 		t.Fatal("failed to set request from request object")
 	}
 	if err := tx.Close(); err != nil {
@@ -60,11 +60,9 @@ func TestProcessRequestEngineOff(t *testing.T) {
 }
 
 func TestProcessRequestMultipart(t *testing.T) {
-	waf := corazawaf.NewWAF()
-	waf.RequestBodyAccess = true
+	waf, _ := coraza.NewWAF(coraza.NewWAFConfig().WithRequestBodyAccess())
 
 	tx := waf.NewTransaction()
-	tx.RequestBodyAccess = true
 
 	req := createMultipartRequest(t)
 
@@ -127,17 +125,20 @@ func createMultipartRequest(t *testing.T) *http.Request {
 // from issue https://github.com/corazawaf/coraza/issues/159 @zpeasystart
 func TestDirectiveSecAuditLog(t *testing.T) {
 	waf := corazawaf.NewWAF()
-	p := seclang.NewParser(waf)
-	if err := p.FromString(`
+	waf.RequestBodyAccess = true
+	if err := seclang.NewParser(waf).FromString(`
 	SecRule REQUEST_FILENAME "@unconditionalMatch" "id:100, phase:2, t:none, log, setvar:'tx.count=+1',chain"
 	SecRule ARGS:username "@unconditionalMatch" "t:none, setvar:'tx.count=+2',chain"
 	SecRule ARGS:password "@unconditionalMatch" "t:none, setvar:'tx.count=+3'"
 		`); err != nil {
-		t.Error(err)
+		t.Fatal(err)
 	}
+	if err := waf.Validate(); err != nil {
+		t.Fatal(err)
+	}
+
 	tx := waf.NewTransaction()
 	defer tx.Close()
-	tx.RequestBodyAccess = true
 	tx.ForceRequestBodyVariable = true
 	// request
 	rdata := []string{
@@ -197,35 +198,36 @@ type debugLogger struct {
 	t *testing.T
 }
 
-func (l *debugLogger) Info(message string, args ...interface{}) { l.t.Logf(message, args...) }
+func (l debugLogger) Info(message string, args ...interface{}) { l.t.Logf(message, args...) }
 
-func (l *debugLogger) Warn(message string, args ...interface{}) { l.t.Logf(message, args...) }
+func (l debugLogger) Warn(message string, args ...interface{}) { l.t.Logf(message, args...) }
 
-func (l *debugLogger) Error(message string, args ...interface{}) { l.t.Logf(message, args...) }
+func (l debugLogger) Error(message string, args ...interface{}) { l.t.Logf(message, args...) }
 
-func (l *debugLogger) Debug(message string, args ...interface{}) { l.t.Logf(message, args...) }
+func (l debugLogger) Debug(message string, args ...interface{}) { l.t.Logf(message, args...) }
 
-func (l *debugLogger) Trace(message string, args ...interface{}) { l.t.Logf(message, args...) }
+func (l debugLogger) Trace(message string, args ...interface{}) { l.t.Logf(message, args...) }
 
-func (l *debugLogger) SetLevel(level loggers.LogLevel) {
+func (l debugLogger) SetLevel(level loggers.LogLevel) {
 	l.t.Logf("Setting level to %q", level.String())
 }
 
-func (l *debugLogger) SetOutput(w io.WriteCloser) {
+func (l debugLogger) SetOutput(w io.WriteCloser) {
 	l.t.Log("ignoring SecDebugLog directive, debug logs are always routed to proxy logs")
 }
 
 type httpTest struct {
-	http2            bool
-	reqURI           string
-	reqBody          string
-	echoReqBody      bool
-	reqBodyLimit     int
-	respHeaders      map[string]string
-	respBody         string
-	expectedProto    string
-	expectedStatus   int
-	expectedRespBody string
+	http2                   bool
+	reqURI                  string
+	reqBody                 string
+	echoReqBody             bool
+	reqBodyLimit            int
+	shouldRejectOnBodyLimit bool
+	respHeaders             map[string]string
+	respBody                string
+	expectedProto           string
+	expectedStatus          int
+	expectedRespBody        string
 }
 
 func TestHttpServer(t *testing.T) {
@@ -252,7 +254,7 @@ func TestHttpServer(t *testing.T) {
 			expectedProto:  "HTTP/1.1",
 			expectedStatus: 403,
 		},
-		"request body larger than limit": {
+		"request body larger than limit (process partial)": {
 			reqURI:      "/hello",
 			reqBody:     "eval('cat /etc/passwd')",
 			echoReqBody: true,
@@ -262,6 +264,16 @@ func TestHttpServer(t *testing.T) {
 			expectedStatus:   201,
 			expectedRespBody: "eval('cat /etc/passwd')",
 		},
+		"request body larger than limit (reject)": {
+			reqURI:                  "/hello",
+			reqBody:                 "something larger than 3",
+			echoReqBody:             true,
+			reqBodyLimit:            3,
+			shouldRejectOnBodyLimit: true,
+			expectedProto:           "HTTP/1.1",
+			expectedStatus:          413,
+			expectedRespBody:        "",
+		},
 		"response headers blocking": {
 			reqURI:         "/hello",
 			respHeaders:    map[string]string{"foo": "bar"},
@@ -287,14 +299,18 @@ func TestHttpServer(t *testing.T) {
 	// Perform tests
 	for name, tCase := range tests {
 		t.Run(name, func(t *testing.T) {
+			limitAction := "ProcessPartial"
+			if tCase.shouldRejectOnBodyLimit {
+				limitAction = "Reject"
+			}
 			conf := coraza.NewWAFConfig().
 				WithDirectives(`
 	# This is a comment
-	SecDebugLogLevel 5
+	SecDebugLogLevel 9
 	SecRequestBodyAccess On
 	SecResponseBodyAccess On
 	SecResponseBodyMimeType text/plain
-	SecRequestBodyLimitAction ProcessPartial
+	SecRequestBodyLimitAction ` + limitAction + `
 	SecRule ARGS:id "@eq 0" "id:1, phase:1,deny, status:403,msg:'Invalid id',log,auditlog"
 	SecRule REQUEST_BODY "@contains eval" "id:100, phase:2,deny, status:403,msg:'Invalid request body',log,auditlog"
 	SecRule RESPONSE_HEADERS:Foo "@pm bar" "id:199,phase:3,deny,t:lowercase,deny, status:401,msg:'Invalid response header',log,auditlog"
@@ -467,3 +483,94 @@ func TestObtainStatusCodeFromInterruptionOrDefault(t *testing.T) {
 		})
 	}
 }
+
+func TestHandlerWithNilWAF(t *testing.T) {
+	delegateHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
+	wrappedHandler := WrapHandler(nil, t.Logf, delegateHandler).(http.HandlerFunc)
+	if want, have := fmt.Sprintf("%v", delegateHandler), fmt.Sprintf("%v", wrappedHandler); want != have {
+		t.Errorf("unexpected wrapped handler")
+	}
+}
+
+func TestHandlerAPI(t *testing.T) {
+	testCases := map[string]struct {
+		handler            http.HandlerFunc
+		expectedStatusCode int
+	}{
+		"empty handler": {
+			handler:            func(w http.ResponseWriter, r *http.Request) {},
+			expectedStatusCode: 200,
+		},
+		"read the request body": {
+			handler: func(w http.ResponseWriter, r *http.Request) {
+				b, err := io.ReadAll(r.Body)
+				if err != nil {
+					panic(err)
+				}
+				if string(b) != "the payload" {
+					panic("unexpected payload")
+				}
+			},
+			expectedStatusCode: 200,
+		},
+		"status code but no body": {
+			handler:            func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(201) },
+			expectedStatusCode: 201,
+		},
+		"double status code but no body": {
+			handler: func(w http.ResponseWriter, r *http.Request) {
+				w.WriteHeader(201)
+				w.WriteHeader(202)
+			},
+			expectedStatusCode: 201,
+		},
+		"no status code and body": {
+			handler: func(w http.ResponseWriter, r *http.Request) {
+				_, _ = w.Write([]byte{1, 2, 3})
+			},
+			expectedStatusCode: 200,
+		},
+		"status code and body": {
+			handler: func(w http.ResponseWriter, r *http.Request) {
+				w.WriteHeader(201)
+				_, _ = w.Write([]byte{1, 2, 3})
+			},
+			expectedStatusCode: 201,
+		},
+		"status code and multiwrite body": {
+			handler: func(w http.ResponseWriter, r *http.Request) {
+				w.WriteHeader(201)
+				_, _ = w.Write([]byte{1, 2, 3})
+				_, _ = w.Write([]byte{4, 5, 6})
+			},
+			expectedStatusCode: 201,
+		},
+	}
+
+	waf, err := coraza.NewWAF(coraza.NewWAFConfig().WithRequestBodyLimit(3))
+	if err != nil {
+		t.Fatalf("unexpected error while creating the WAF: %s", err.Error())
+	}
+
+	for name, tCase := range testCases {
+		t.Run(name, func(t *testing.T) {
+			srv := httptest.NewServer(WrapHandler(waf, t.Logf, tCase.handler))
+			defer srv.Close()
+
+			res, err := http.Post(srv.URL, "application/json", bytes.NewBufferString("the payload"))
+			if err != nil {
+				t.Fatalf("unexpected error while performing the request: %s", err.Error())
+			}
+			defer res.Body.Close()
+
+			if want, have := tCase.expectedStatusCode, res.StatusCode; want != have {
+				t.Fatalf("unexpected status code, want: %d, have: %d", want, have)
+			}
+
+			_, err = io.ReadAll(res.Body)
+			if err != nil {
+				t.Fatalf("unexpected error while reading the body: %s", err.Error())
+			}
+		})
+	}
+}
diff --git a/internal/collections/concat_test.go b/internal/collections/concat_test.go
new file mode 100644
index 0000000..b19af0d
--- /dev/null
+++ b/internal/collections/concat_test.go
@@ -0,0 +1,93 @@
+// Copyright 2023 Juan Pablo Tosso and the OWASP Coraza contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package collections
+
+import (
+	"regexp"
+	"strings"
+	"testing"
+
+	"github.com/corazawaf/coraza/v3/types"
+	"github.com/corazawaf/coraza/v3/types/variables"
+)
+
+func TestConcatKeyed(t *testing.T) {
+	c1 := NewMap(variables.ArgsGet)
+	c2 := NewMap(variables.ArgsPost)
+	c3 := NewMap(variables.ArgsPath)
+
+	c := NewConcatKeyed(variables.Args, c1, c2, c3)
+
+	re1 := regexp.MustCompile("anim")
+	re2 := regexp.MustCompile("pla")
+
+	if want, have := "ARGS", c.Name(); want != have {
+		t.Errorf("want %q, have %q", want, have)
+	}
+
+	assertValuesMatch(t, c.FindAll())
+
+	c1.Add("animal", "cat")
+
+	assertValuesMatch(t, c.FindAll(), "cat")
+	assertValuesMatch(t, c.FindString("animal"), "cat")
+	assertValuesMatch(t, c.FindString("plant"))
+	assertValuesMatch(t, c.FindRegex(re1), "cat")
+	assertValuesMatch(t, c.FindRegex(re2))
+
+	c2.Add("animal", "dog")
+
+	assertValuesMatch(t, c.FindAll(), "cat", "dog")
+	assertValuesMatch(t, c.FindString("animal"), "cat", "dog")
+	assertValuesMatch(t, c.FindString("plant"))
+	assertValuesMatch(t, c.FindRegex(re1), "cat", "dog")
+	assertValuesMatch(t, c.FindRegex(re2))
+
+	c3.Add("plant", "palm")
+
+	assertValuesMatch(t, c.FindAll(), "cat", "dog", "palm")
+	assertValuesMatch(t, c.FindString("animal"), "cat", "dog")
+	assertValuesMatch(t, c.FindString("plant"), "palm")
+	assertValuesMatch(t, c.FindRegex(re1), "cat", "dog")
+	assertValuesMatch(t, c.FindRegex(re2), "palm")
+}
+
+func TestConcatCollection(t *testing.T) {
+	c1 := NewMap(variables.ArgsGet)
+	c2 := NewMap(variables.ArgsPost)
+	c3 := NewMap(variables.ArgsPath)
+
+	c := NewConcatCollection(variables.Args, c1, c2, c3)
+
+	if want, have := "ARGS", c.Name(); want != have {
+		t.Errorf("want %q, have %q", want, have)
+	}
+
+	assertValuesMatch(t, c.FindAll())
+
+	c1.Add("animal", "cat")
+
+	assertValuesMatch(t, c.FindAll(), "cat")
+
+	c2.Add("animal", "dog")
+
+	assertValuesMatch(t, c.FindAll(), "cat", "dog")
+
+	c3.Add("plant", "palm")
+
+	assertValuesMatch(t, c.FindAll(), "cat", "dog", "palm")
+}
+
+func assertValuesMatch(t *testing.T, matches []types.MatchData, wantValues ...string) {
+	t.Helper()
+
+	haveValues := make([]string, len(matches))
+	for i := range matches {
+		haveValues[i] = matches[i].Value()
+	}
+	// String concat is the simplest way to compare and print a message without reflection
+	if want, have := strings.Join(wantValues, ","), strings.Join(haveValues, ","); want != have {
+		t.Errorf("want %q, have %q", want, have)
+	}
+}
diff --git a/collection/collection_map_test.go b/internal/collections/map_test.go
similarity index 65%
rename from collection/collection_map_test.go
rename to internal/collections/map_test.go
index 8ad68eb..346e798 100644
--- a/collection/collection_map_test.go
+++ b/internal/collections/map_test.go
@@ -11,20 +11,21 @@
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
-package collection
+package collections
 
 import (
+	"fmt"
 	"regexp"
 	"testing"
 
 	"github.com/corazawaf/coraza/v3/types/variables"
 )
 
-func TestCollectionMap(t *testing.T) {
+func TestMap(t *testing.T) {
 	c := NewMap(variables.ArgsPost)
 	c.SetIndex("key", 1, "value")
 	c.Set("key2", []string{"value2"})
-	if c.Get("key")[1] != "value" {
+	if c.Get("key")[0] != "value" {
 		t.Error("Error setting index")
 	}
 	if len(c.FindAll()) == 0 {
@@ -33,7 +34,25 @@ func TestCollectionMap(t *testing.T) {
 	if len(c.FindString("a")) > 0 {
 		t.Error("Error should not find string")
 	}
-	if l := len(c.FindRegex(regexp.MustCompile("k.*"))); l != 3 {
+	if l := len(c.FindRegex(regexp.MustCompile("k.*"))); l != 2 {
 		t.Errorf("Error should find regex, got %d", l)
 	}
+
+	c.Add("key2", "value3")
+
+	wantStr := `ARGS_POST:
+    key: value
+    key2: value2,value3
+`
+
+	if have := fmt.Sprint(c); have != wantStr {
+		// Map order is not guaranteed, not pretty but checking twice is the simplest for now.
+		wantStr = `ARGS_POST:
+    key2: value2,value3
+    key: value
+`
+		if have != wantStr {
+			t.Errorf("String() = %q, want %q", have, wantStr)
+		}
+	}
 }
diff --git a/internal/collections/named_test.go b/internal/collections/named_test.go
new file mode 100644
index 0000000..ee19127
--- /dev/null
+++ b/internal/collections/named_test.go
@@ -0,0 +1,75 @@
+// Copyright 2023 Juan Pablo Tosso and the OWASP Coraza contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package collections
+
+import (
+	"fmt"
+	"regexp"
+	"testing"
+
+	"github.com/corazawaf/coraza/v3/types/variables"
+)
+
+func TestNamedCollection(t *testing.T) {
+	c := NewNamedCollection(variables.ArgsPost)
+
+	// Same as collection_map_test
+	c.SetIndex("key", 1, "value")
+	c.Set("key2", []string{"value2"})
+	if c.Get("key")[0] != "value" {
+		t.Error("Error setting index")
+	}
+	if len(c.FindAll()) == 0 {
+		t.Error("Error finding all")
+	}
+	if len(c.FindString("a")) > 0 {
+		t.Error("Error should not find string")
+	}
+	if l := len(c.FindRegex(regexp.MustCompile("k.*"))); l != 2 {
+		t.Errorf("Error should find regex, got %d", l)
+	}
+	wantStr := `ARGS_POST:
+    key: value
+    key2: value2
+`
+	if have := fmt.Sprint(c); have != wantStr {
+		// Map order is not guaranteed, not pretty but checking twice is the simplest for now.
+		wantStr = `ARGS_POST:
+    key2: value2
+    key: value
+`
+		if have != wantStr {
+			t.Errorf("String() = %q, want %q", have, wantStr)
+		}
+	}
+
+	// Now test names
+
+	names := c.Names(variables.ArgsPostNames)
+	if want, have := "ARGS_POST_NAMES", names.Name(); want != have {
+		t.Errorf("want %q, have %q", want, have)
+	}
+
+	assertValuesMatch(t, names.FindAll(), "key", "key2")
+	if want, have := "ARGS_POST_NAMES: key,key2", fmt.Sprint(names); want != have {
+		t.Errorf("want %q, have %q", want, have)
+	}
+	c.Add("key", "value2")
+	assertValuesMatch(t, names.FindAll(), "key", "key2")
+	if want, have := "ARGS_POST_NAMES: key,key2", fmt.Sprint(names); want != have {
+		t.Errorf("want %q, have %q", want, have)
+	}
+	// While selection operators will treat this as case-insensitive, names should have all names
+	// as-is.
+	c.Add("Key", "value3")
+	assertValuesMatch(t, names.FindAll(), "key", "key2", "Key")
+	if want, have := "ARGS_POST_NAMES: key,key2,Key", fmt.Sprint(names); want != have {
+		t.Errorf("want %q, have %q", want, have)
+	}
+	c.Remove("key2")
+	assertValuesMatch(t, names.FindAll(), "key", "Key")
+	if want, have := "ARGS_POST_NAMES: key,Key", fmt.Sprint(names); want != have {
+		t.Errorf("want %q, have %q", want, have)
+	}
+}
diff --git a/internal/collections/single_test.go b/internal/collections/single_test.go
new file mode 100644
index 0000000..8e2ef01
--- /dev/null
+++ b/internal/collections/single_test.go
@@ -0,0 +1,40 @@
+// Copyright 2023 Juan Pablo Tosso and the OWASP Coraza contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package collections
+
+import (
+	"testing"
+
+	"github.com/corazawaf/coraza/v3/types/variables"
+)
+
+func TestSingle(t *testing.T) {
+	c := NewSingle(variables.ArgsPath)
+
+	if want, have := "ARGS_PATH", c.Name(); want != have {
+		t.Errorf("want %s, have %s", want, have)
+	}
+
+	if want, have := "", c.Get(); want != have {
+		t.Errorf("want %s, have %s", want, have)
+	}
+
+	if want, have := "ARGS_PATH: ", c.String(); want != have {
+		t.Errorf("want %s, have %s", want, have)
+	}
+
+	assertValuesMatch(t, c.FindAll())
+
+	c.Set("bear")
+
+	if want, have := "bear", c.Get(); want != have {
+		t.Errorf("want %s, have %s", want, have)
+	}
+
+	if want, have := "ARGS_PATH: bear", c.String(); want != have {
+		t.Errorf("want %s, have %s", want, have)
+	}
+
+	assertValuesMatch(t, c.FindAll(), "bear")
+}
diff --git a/internal/collections/sized_test.go b/internal/collections/sized_test.go
new file mode 100644
index 0000000..c90705d
--- /dev/null
+++ b/internal/collections/sized_test.go
@@ -0,0 +1,36 @@
+// Copyright 2023 Juan Pablo Tosso and the OWASP Coraza contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package collections
+
+import (
+	"testing"
+
+	"github.com/corazawaf/coraza/v3/types/variables"
+)
+
+func TestSizedCollection(t *testing.T) {
+	c1 := NewNamedCollection(variables.ArgsPost)
+	c2 := NewNamedCollection(variables.ArgsGet)
+	proxy := NewSizeCollection(variables.ArgsCombinedSize, c1, c2)
+
+	assertValuesMatch(t, proxy.FindAll(), "0")
+	if want, have := "ARGS_COMBINED_SIZE: 0", proxy.String(); want != have {
+		t.Errorf("want %s, have %s", want, have)
+	}
+	c1.Set("key1", []string{"value1", "value2"})
+	assertValuesMatch(t, proxy.FindAll(), "12")
+	if want, have := "ARGS_COMBINED_SIZE: 12", proxy.String(); want != have {
+		t.Errorf("want %s, have %s", want, have)
+	}
+	c1.Set("key2", []string{"value2"})
+	assertValuesMatch(t, proxy.FindAll(), "18")
+	if want, have := "ARGS_COMBINED_SIZE: 18", proxy.String(); want != have {
+		t.Errorf("want %s, have %s", want, have)
+	}
+	c2.Set("key3", []string{"value3"})
+	assertValuesMatch(t, proxy.FindAll(), "24")
+	if want, have := "ARGS_COMBINED_SIZE: 24", proxy.String(); want != have {
+		t.Errorf("want %s, have %s", want, have)
+	}
+}
diff --git a/internal/corazawaf/logger_test.go b/internal/corazawaf/logger_test.go
new file mode 100644
index 0000000..c1578f6
--- /dev/null
+++ b/internal/corazawaf/logger_test.go
@@ -0,0 +1,66 @@
+// Copyright 2022 Juan Pablo Tosso and the OWASP Coraza contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package corazawaf
+
+import (
+	"testing"
+
+	"github.com/corazawaf/coraza/v3/loggers"
+)
+
+func TestLoggerLogLevels(t *testing.T) {
+	waf := NewWAF()
+	testCases := map[string]struct {
+		logFunction                func(message string, args ...interface{})
+		expectedLowestPrintedLevel int
+	}{
+		"Trace": {
+			logFunction:                waf.Logger.Trace,
+			expectedLowestPrintedLevel: 9,
+		},
+		"Debug": {
+			logFunction:                waf.Logger.Debug,
+			expectedLowestPrintedLevel: 4,
+		},
+		"Info": {
+			logFunction:                waf.Logger.Info,
+			expectedLowestPrintedLevel: 3,
+		},
+		"Warn": {
+			logFunction:                waf.Logger.Warn,
+			expectedLowestPrintedLevel: 2,
+		},
+		"Error": {
+			logFunction:                waf.Logger.Error,
+			expectedLowestPrintedLevel: 1,
+		},
+	}
+
+	for name, tCase := range testCases {
+		t.Run(name, func(t *testing.T) {
+			for settedLevel := 0; settedLevel <= 9; settedLevel++ {
+				l := &inspectableLogger{}
+				waf.Logger.SetOutput(l)
+				waf.Logger.SetLevel(loggers.LogLevel(settedLevel))
+				tCase.logFunction("this is a log")
+
+				if settedLevel >= tCase.expectedLowestPrintedLevel && len(l.entries) != 1 {
+					t.Fatalf("Missing expected log. Level: %d, Function: %s", settedLevel, name)
+				}
+				if settedLevel < tCase.expectedLowestPrintedLevel && len(l.entries) == 1 {
+					t.Fatalf("Unexpected log. Level: %d, Function: %s", settedLevel, name)
+				}
+			}
+
+		})
+	}
+}
+
+func TestLoggerLevelDefaultsToInfo(t *testing.T) {
+	waf := NewWAF()
+	waf.Logger.SetLevel(loggers.LogLevel(10))
+	if waf.Logger.(*stdDebugLogger).Level != loggers.LogLevelInfo {
+		t.Fatalf("Unexpected log level: %d. It should default to Info (%s)", waf.Logger.(*stdDebugLogger).Level, loggers.LogLevelInfo)
+	}
+}
diff --git a/internal/corazawaf/transaction_test.go b/internal/corazawaf/transaction_test.go
index 3e13ba4..fbe93fc 100644
--- a/internal/corazawaf/transaction_test.go
+++ b/internal/corazawaf/transaction_test.go
@@ -13,6 +13,7 @@ import (
 	"testing"
 
 	"github.com/corazawaf/coraza/v3/collection"
+	"github.com/corazawaf/coraza/v3/internal/collections"
 	"github.com/corazawaf/coraza/v3/internal/corazarules"
 	utils "github.com/corazawaf/coraza/v3/internal/strings"
 	"github.com/corazawaf/coraza/v3/loggers"
@@ -198,7 +199,6 @@ func TestWriteRequestBody(t *testing.T) {
 							waf.RuleEngine = types.RuleEngineOn
 							waf.RequestBodyAccess = true
 							waf.RequestBodyLimit = int64(testCase.requestBodyLimit)
-							waf.RequestBodyInMemoryLimit = int64(testCase.requestBodyLimit)
 							waf.RequestBodyLimitAction = testCase.requestBodyLimitAction
 
 							tx := waf.NewTransaction()
@@ -269,7 +269,6 @@ func TestWriteRequestBodyOnLimitReached(t *testing.T) {
 		waf.RuleEngine = types.RuleEngineOn
 		waf.RequestBodyAccess = true
 		waf.RequestBodyLimit = 2
-		waf.RequestBodyInMemoryLimit = 2
 		waf.RequestBodyLimitAction = tCase.requestBodyLimitAction
 
 		t.Run(tName, func(t *testing.T) {
@@ -352,7 +351,7 @@ func TestWriteRequestBodyIsNopWhenBodyIsNotAccesible(t *testing.T) {
 func TestResponseHeader(t *testing.T) {
 	tx := makeTransaction(t)
 	tx.AddResponseHeader("content-type", "test")
-	if tx.variables.responseContentType.String() != "test" {
+	if tx.variables.responseContentType.Get() != "test" {
 		t.Error("invalid RESPONSE_CONTENT_TYPE after response headers")
 	}
 
@@ -487,10 +486,15 @@ func TestWriteResponseBody(t *testing.T) {
 						t.Run(name, func(t *testing.T) {
 							waf := NewWAF()
 							waf.RuleEngine = types.RuleEngineOn
+							waf.ResponseBodyMimeTypes = []string{"text/plain"}
 							waf.ResponseBodyAccess = true
 							waf.ResponseBodyLimit = int64(testCase.responseBodyLimit)
 							waf.ResponseBodyLimitAction = testCase.responseBodyLimitAction
 
+							if err := waf.Validate(); err != nil {
+								t.Fatalf("failed to validate the WAF: %s", err.Error())
+							}
+
 							tx := waf.NewTransaction()
 							tx.AddResponseHeader("content-type", "text/plain")
 
@@ -522,7 +526,7 @@ func TestWriteResponseBody(t *testing.T) {
 								}
 								// checking if the body has been populated up to the first POST arg
 								index := strings.Index(urlencodedBody, "&")
-								if tx.variables.responseBody.String()[:index] != urlencodedBody[:index] {
+								if tx.variables.responseBody.Get()[:index] != urlencodedBody[:index] {
 									t.Error("failed to set response body")
 								}
 							}
@@ -732,10 +736,10 @@ func TestHeaderSetters(t *testing.T) {
 	if tx.variables.requestHeaders.Get("cookie")[0] != "abc=def;hij=klm" {
 		t.Error("failed to set request header")
 	}
-	if !utils.InSlice("cookie", tx.variables.requestHeadersNames.Get("cookie")) {
-		t.Error("failed to set header name", tx.variables.requestHeadersNames.Get("cookie"))
+	if !utils.InSlice("cookie", collectionValues(t, tx.variables.requestHeadersNames)) {
+		t.Error("failed to set header name", collectionValues(t, tx.variables.requestHeadersNames))
 	}
-	if !utils.InSlice("abc", tx.variables.requestCookiesNames.Get("abc")) {
+	if !utils.InSlice("abc", collectionValues(t, tx.variables.requestCookiesNames)) {
 		t.Error("failed to set cookie name")
 	}
 	if err := tx.Close(); err != nil {
@@ -743,6 +747,15 @@ func TestHeaderSetters(t *testing.T) {
 	}
 }
 
+func collectionValues(t *testing.T, col collection.Collection) []string {
+	t.Helper()
+	var values []string
+	for _, v := range col.FindAll() {
+		values = append(values, v.Value())
+	}
+	return values
+}
+
 func TestRequestBodyProcessingAlgorithm(t *testing.T) {
 	waf := NewWAF()
 	tx := waf.NewTransaction()
@@ -757,7 +770,7 @@ func TestRequestBodyProcessingAlgorithm(t *testing.T) {
 	if _, err := tx.ProcessRequestBody(); err != nil {
 		t.Error("failed to process request body")
 	}
-	if tx.variables.requestBody.String() != "test123" {
+	if tx.variables.requestBody.Get() != "test123" {
 		t.Error("failed to set request body")
 	}
 	if err := tx.Close(); err != nil {
@@ -861,6 +874,7 @@ func TestTxPhase4Magic(t *testing.T) {
 	waf.ResponseBodyAccess = true
 	waf.ResponseBodyLimit = 3
 	waf.ResponseBodyLimitAction = types.BodyLimitActionProcessPartial
+	waf.ResponseBodyMimeTypes = []string{"text/html"}
 	tx := waf.NewTransaction()
 	tx.AddResponseHeader("content-type", "text/html")
 	if it, _, err := tx.WriteResponseBody([]byte("more bytes")); it != nil || err != nil {
@@ -869,10 +883,10 @@ func TestTxPhase4Magic(t *testing.T) {
 	if _, err := tx.ProcessResponseBody(); err != nil {
 		t.Error(err)
 	}
-	if tx.variables.outboundDataError.String() != "1" {
+	if tx.variables.outboundDataError.Get() != "1" {
 		t.Error("failed to set outbound data error")
 	}
-	if tx.variables.responseBody.String() != "mor" {
+	if tx.variables.responseBody.Get() != "mor" {
 		t.Error("failed to set response body")
 	}
 }
@@ -892,7 +906,7 @@ func TestVariablesMatch(t *testing.T) {
 	}
 
 	for k, v := range expect {
-		if m := (tx.Collection(k)).(*collection.Simple).String(); m != v {
+		if m := (tx.Collection(k)).(*collections.Single).Get(); m != v {
 			t.Errorf("failed to match variable %s, Expected: %s, got: %s", k.Name(), v, m)
 		}
 	}
@@ -913,7 +927,7 @@ func TestTxReqBodyForce(t *testing.T) {
 	if _, err := tx.ProcessRequestBody(); err != nil {
 		t.Error(err)
 	}
-	if tx.variables.requestBody.String() != "test" {
+	if tx.variables.requestBody.Get() != "test" {
 		t.Error("failed to set request body")
 	}
 }
@@ -929,7 +943,7 @@ func TestTxReqBodyForceNegative(t *testing.T) {
 	if _, err := tx.ProcessRequestBody(); err != nil {
 		t.Error(err)
 	}
-	if tx.variables.requestBody.String() == "test" {
+	if tx.variables.requestBody.Get() == "test" {
 		t.Error("reqbody should not be there")
 	}
 }
@@ -938,14 +952,36 @@ func TestTxProcessConnection(t *testing.T) {
 	waf := NewWAF()
 	tx := waf.NewTransaction()
 	tx.ProcessConnection("127.0.0.1", 80, "127.0.0.2", 8080)
-	if tx.variables.remoteAddr.String() != "127.0.0.1" {
+	if tx.variables.remoteAddr.Get() != "127.0.0.1" {
 		t.Error("failed to set client ip")
 	}
-	if tx.variables.remotePort.Int() != 80 {
+	if rp, _ := strconv.Atoi(tx.variables.remotePort.Get()); rp != 80 {
 		t.Error("failed to set client port")
 	}
 }
 
+func TestTxSetServerName(t *testing.T) {
+
+	l := &inspectableLogger{}
+	waf := NewWAF()
+	waf.Logger.SetOutput(l)
+	waf.Logger.SetLevel(loggers.LogLevelWarn)
+	tx := waf.NewTransaction()
+	tx.LastPhase = types.PhaseRequestHeaders
+	tx.SetServerName("coraza.io")
+	if tx.variables.serverName.Get() != "coraza.io" {
+		t.Error("failed to set server name")
+	}
+	if want, have := 1, len(l.entries); want != have {
+		t.Fatalf("unexpected number of log entries, want %d, have %d", want, have)
+	}
+
+	if want, have := "SetServerName has been called after ProcessRequestHeaders", l.entries[0]; !strings.Contains(have, want) {
+		t.Fatalf("unexpected message, want %q, have %q", want, have)
+	}
+
+}
+
 func TestTxAddArgument(t *testing.T) {
 	waf := NewWAF()
 	tx := waf.NewTransaction()
@@ -980,13 +1016,13 @@ func TestTxProcessURI(t *testing.T) {
 	tx := waf.NewTransaction()
 	uri := "http://example.com/path/to/file.html?query=string&other=value"
 	tx.ProcessURI(uri, "GET", "HTTP/1.1")
-	if s := tx.variables.requestURI.String(); s != uri {
+	if s := tx.variables.requestURI.Get(); s != uri {
 		t.Errorf("failed to set request uri, got %s", s)
 	}
-	if s := tx.variables.requestBasename.String(); s != "file.html" {
+	if s := tx.variables.requestBasename.Get(); s != "file.html" {
 		t.Errorf("failed to set request path, got %s", s)
 	}
-	if tx.variables.queryString.String() != "query=string&other=value" {
+	if tx.variables.queryString.Get() != "query=string&other=value" {
 		t.Error("failed to set request query")
 	}
 	if v := tx.variables.args.FindAll(); len(v) != 2 {
@@ -1074,7 +1110,7 @@ func validateMacroExpansion(tests map[string]string, tx *Transaction, t *testing
 		res := m.Expand(tx)
 		if res != v {
 			if testing.Verbose() {
-				fmt.Println(tx.Debug())
+				fmt.Println(tx)
 				fmt.Println("===STACK===\n", string(debug.Stack())+"\n===STACK===")
 			}
 			t.Error("Failed set transaction for " + k + ", expected " + v + ", got " + res)
diff --git a/internal/corazawaf/waf_test.go b/internal/corazawaf/waf_test.go
index f6af2fc..a98e0c0 100644
--- a/internal/corazawaf/waf_test.go
+++ b/internal/corazawaf/waf_test.go
@@ -70,3 +70,59 @@ func TestSetDebugLogPath(t *testing.T) {
 		})
 	}
 }
+
+func TestValidate(t *testing.T) {
+	testCases := map[string]struct {
+		customizer func(*WAF)
+		expectErr  bool
+	}{
+		"default": {
+			expectErr:  false,
+			customizer: func(w *WAF) {},
+		},
+		"request body limit less than zero": {
+			expectErr:  true,
+			customizer: func(w *WAF) { w.RequestBodyLimit = -1 },
+		},
+		"request body limit greater than 1gb": {
+			expectErr:  true,
+			customizer: func(w *WAF) { w.RequestBodyLimit = _1gb + 1 },
+		},
+		"request body in memory limit less than zero": {
+			expectErr:  true,
+			customizer: func(w *WAF) { w.SetRequestBodyInMemoryLimit(-1) },
+		},
+		"request body limit less than request body in memory limit": {
+			expectErr: true,
+			customizer: func(w *WAF) {
+				w.RequestBodyLimit = 10
+				w.SetRequestBodyInMemoryLimit(11)
+			}},
+		"response body limit less than zero": {
+			expectErr:  true,
+			customizer: func(w *WAF) { w.ResponseBodyLimit = -1 },
+		},
+		"response body limit greater than 1gb": {
+			expectErr:  true,
+			customizer: func(w *WAF) { w.ResponseBodyLimit = _1gb + 1 },
+		},
+	}
+
+	for name, tCase := range testCases {
+		t.Run(name, func(t *testing.T) {
+			waf := NewWAF()
+			tCase.customizer(waf)
+			err := waf.Validate()
+			if tCase.expectErr {
+				if err == nil {
+					t.Fatalf("expected error: %s", err.Error())
+				}
+			} else {
+				if err != nil {
+					t.Fatalf("unexpected error: %s", err.Error())
+				}
+			}
+
+		})
+	}
+}
diff --git a/internal/seclang/directives_log_test.go b/internal/seclang/directives_log_test.go
index 923bd45..3dee27c 100644
--- a/internal/seclang/directives_log_test.go
+++ b/internal/seclang/directives_log_test.go
@@ -80,7 +80,7 @@ func TestDebugDirectives(t *testing.T) {
 	}
 	if err := directiveSecDebugLogLevel(&DirectiveOptions{
 		WAF:  waf,
-		Opts: "5",
+		Opts: "3",
 	}); err != nil {
 		t.Error(err)
 	}
diff --git a/internal/seclang/directives_test.go b/internal/seclang/directives_test.go
index 87e619e..129520e 100644
--- a/internal/seclang/directives_test.go
+++ b/internal/seclang/directives_test.go
@@ -4,6 +4,7 @@
 package seclang
 
 import (
+	"strings"
 	"testing"
 
 	"github.com/corazawaf/coraza/v3/internal/corazawaf"
@@ -33,110 +34,6 @@ func Test_NonImplementedDirective(t *testing.T) {
 	}
 }
 
-func Test_directive(t *testing.T) {
-	w := corazawaf.NewWAF()
-	p := NewParser(w)
-	if err := p.FromString("SecWebAppId test123"); err != nil {
-		t.Error("failed to set parser from string")
-	}
-	if w.WebAppID != "test123" {
-		t.Error("failed to set SecWebAppId")
-	}
-	if err := p.FromString("SecUploadKeepFiles On"); err != nil {
-		t.Error("failed to set parser from string")
-	}
-	if !w.UploadKeepFiles {
-		t.Error("failed to set SecUploadKeepFiles")
-	}
-	if err := p.FromString("SecUploadFileMode 0700"); err != nil {
-		t.Error("failed to set parser from string")
-	}
-	if err := p.FromString("SecUploadFileLimit 1000"); err != nil {
-		t.Error("failed to set parser from string")
-	}
-	if w.UploadFileLimit != 1000 {
-		t.Error("failed to set SecUploadFileLimit")
-	}
-	if err := p.FromString("SecUploadDir /tmp"); err != nil {
-		t.Error("failed to set parser from string")
-	}
-	if w.UploadDir != "/tmp" {
-		t.Error("failed to set SecUploadDir")
-	}
-	if err := p.FromString("SecTmpDir /tmp"); err != nil {
-		t.Error("failed to set parser from string")
-	}
-	if w.TmpDir != "/tmp" {
-		t.Error("failed to set SecTmpDir")
-	}
-	if err := p.FromString("SecSensorId test"); err != nil {
-		t.Error("failed to set parser from string")
-	}
-	if w.SensorID != "test" {
-		t.Error("failed to set SecSensorId")
-	}
-	if err := p.FromString("SecRuleEngine DetectionOnly"); err != nil {
-		t.Error("failed to set parser from string")
-	}
-	if w.RuleEngine != types.RuleEngineDetectionOnly {
-		t.Errorf("failed to set SecRuleEngine, got %s and expected %s", w.RuleEngine.String(), types.RuleEngineDetectionOnly.String())
-	}
-	if err := p.FromString(`SecAction "id:1,tag:test"`); err != nil {
-		t.Error("failed to set parser from string")
-	}
-	if err := p.FromString("SecRuleRemoveByTag test"); err != nil {
-		t.Error("failed to set parser from string")
-	}
-	if p.options.WAF.Rules.Count() != 0 {
-		t.Error("Failed to remove rule with SecRuleRemoveByTag")
-	}
-	if err := p.FromString(`SecAction "id:1,msg:'test'"`); err != nil {
-		t.Error("failed to set parser from string")
-	}
-	if err := p.FromString("SecRuleRemoveByMsg test"); err != nil {
-		t.Error("failed to set parser from string")
-	}
-	if p.options.WAF.Rules.Count() != 0 {
-		t.Error("Failed to remove rule with SecRuleRemoveByMsg")
-	}
-	if err := p.FromString(`SecAction "id:1"`); err != nil {
-		t.Error("failed to set parser from string")
-	}
-	if err := p.FromString("SecRuleRemoveById 1"); err != nil {
-		t.Error("failed to set parser from string")
-	}
-	if p.options.WAF.Rules.Count() != 0 {
-		t.Error("Failed to remove rule with SecRuleRemoveById")
-	}
-	if err := p.FromString("SecResponseBodyMimeTypesClear"); err != nil {
-		t.Error("failed to set parser from string")
-	}
-	if len(p.options.WAF.ResponseBodyMimeTypes) != 0 {
-		t.Error("failed to set SecResponseBodyMimeTypesClear")
-	}
-	if err := p.FromString("SecResponseBodyMimeType text/html"); err != nil {
-		t.Error("failed to set parser from string")
-	}
-	if p.options.WAF.ResponseBodyMimeTypes[0] != "text/html" {
-		t.Error("failed to set SecResponseBodyMimeType")
-	}
-	if err := p.FromString(`SecServerSignature "Microsoft-IIS/6.0"`); err != nil {
-		t.Error("failed to set directive: SecServerSignature")
-	}
-	if err := p.FromString(`SecRequestBodyInMemoryLimit 131072`); err != nil {
-		t.Error("failed to set directive: SecRequestBodyInMemoryLimit")
-	}
-	if err := p.FromString(`SecRemoteRulesFailAction Abort`); err != nil {
-		t.Error("failed to set directive: SecRemoteRulesFailAction")
-	}
-	if err := p.FromString(`SecRequestBodyLimitAction Reject`); err != nil {
-		t.Error("failed to set directive: SecRequestBodyLimitAction")
-	}
-	if err := p.FromString(`SecResponseBodyLimitAction ProcessPartial`); err != nil {
-		t.Error("failed to set directive: SecResponseBodyLimitAction")
-	}
-}
-
 func TestSecRuleUpdateTargetBy(t *testing.T) {
 	waf := corazawaf.NewWAF()
 	rule, err := ParseRule(RuleOptions{
@@ -203,3 +100,166 @@ func TestSecDataset(t *testing.T) {
 		t.Error("failed to add dataset")
 	}
 }
+
+var expectErrorOnDirective func(*corazawaf.WAF) bool = nil
+
+func TestDirectives(t *testing.T) {
+	type directiveCase struct {
+		opts  string
+		check func(*corazawaf.WAF) bool
+	}
+	directiveCases := map[string][]directiveCase{
+		"SecComponentSignature": {
+			{"", expectErrorOnDirective},
+			{"name", func(w *corazawaf.WAF) bool { return len(w.ComponentNames) == 1 }},
+		},
+		"SecMarker": {
+			{"", expectErrorOnDirective},
+			{"999", func(w *corazawaf.WAF) bool { return w.Rules.Count() == 1 }},
+			{"MY_TEXT", func(w *corazawaf.WAF) bool { return w.Rules.Count() == 1 }},
+		},
+		"SecWebAppId": {
+			{"", expectErrorOnDirective},
+			{"test123", func(w *corazawaf.WAF) bool { return w.WebAppID == "test123" }},
+		},
+		"SecUploadKeepFiles": {
+			{"", expectErrorOnDirective},
+			{"Ox", expectErrorOnDirective},
+			{"On", func(w *corazawaf.WAF) bool { return w.UploadKeepFiles }},
+			{"Off", func(w *corazawaf.WAF) bool { return !w.UploadKeepFiles }},
+		},
+		"SecUploadFileMode": {
+			{"", expectErrorOnDirective},
+			{"888", expectErrorOnDirective},
+			{"700", func(w *corazawaf.WAF) bool { return w.UploadFileMode == 0700 }},
+		},
+		"SecUploadFileLimit": {
+			{"", expectErrorOnDirective},
+			{"1000", func(w *corazawaf.WAF) bool { return w.UploadFileLimit == 1000 }},
+		},
+		"SecUploadDir": {
+			{"", expectErrorOnDirective},
+			{"/tmp", func(w *corazawaf.WAF) bool { return w.UploadDir == "/tmp" }},
+		},
+		"SecTmpDir": {
+			{"", expectErrorOnDirective},
+			{"/tmp", func(w *corazawaf.WAF) bool { return w.TmpDir == "/tmp" }},
+		},
+		"SecSensorId": {
+			{"", expectErrorOnDirective},
+			{"test", func(w *corazawaf.WAF) bool { return w.SensorID == "test" }},
+		},
+		"SecRuleEngine": {
+			{"What?", expectErrorOnDirective},
+			{"DetectionOnly", func(w *corazawaf.WAF) bool { return w.RuleEngine == types.RuleEngineDetectionOnly }},
+			{"On", func(w *corazawaf.WAF) bool { return w.RuleEngine == types.RuleEngineOn }},
+			{"Off", func(w *corazawaf.WAF) bool { return w.RuleEngine == types.RuleEngineOff }},
+		},
+		"SecAction": {
+			{"", expectErrorOnDirective},
+			{`"id:1,tag:test"`, func(w *corazawaf.WAF) bool { return w.Rules.Count() == 1 }},
+		},
+		"SecRuleRemoveByTag": {
+			{"", expectErrorOnDirective},
+		},
+		"SecRuleRemoveByMsg": {
+			{"", expectErrorOnDirective},
+		},
+		"SecRuleRemoveById": {
+			{"", expectErrorOnDirective},
+		},
+		"SecResponseBodyMimeTypesClear": {
+			{"", func(w *corazawaf.WAF) bool { return len(w.ResponseBodyMimeTypes) == 0 }},
+			{"x", expectErrorOnDirective},
+		},
+		"SecResponseBodyMimeType": {
+			{"", expectErrorOnDirective},
+			{"text/html", func(w *corazawaf.WAF) bool { return w.ResponseBodyMimeTypes[0] == "text/html" }},
+		},
+		"SecServerSignature": {
+			{"", expectErrorOnDirective},
+			{`"Microsoft-IIS/6.0"`, func(w *corazawaf.WAF) bool { return w.ServerSignature == "Microsoft-IIS/6.0" }},
+		},
+		"SecRequestBodyLimit": {
+			{"", expectErrorOnDirective},
+			{"x", expectErrorOnDirective},
+			{"123", func(w *corazawaf.WAF) bool { return w.RequestBodyLimit == 123 }},
+		},
+		"SecResponseBodyLimit": {
+			{"", expectErrorOnDirective},
+			{"y", expectErrorOnDirective},
+			{"123", func(w *corazawaf.WAF) bool { return w.ResponseBodyLimit == 123 }},
+		},
+		"SecRequestBodyInMemoryLimit": {
+			{"", expectErrorOnDirective},
+			{"z", expectErrorOnDirective},
+			{"123", func(w *corazawaf.WAF) bool { return *(w.RequestBodyInMemoryLimit()) == 123 }},
+		},
+		"SecRequestBodyLimitAction": {
+			{"", expectErrorOnDirective},
+			{"What?", expectErrorOnDirective},
+			{"Reject", func(w *corazawaf.WAF) bool { return w.RequestBodyLimitAction == types.BodyLimitActionReject }},
+			{"ProcessPartial", func(w *corazawaf.WAF) bool { return w.RequestBodyLimitAction == types.BodyLimitActionProcessPartial }},
+		},
+		"SecRequestBodyAccess": {
+			{"", expectErrorOnDirective},
+			{"What?", expectErrorOnDirective},
+			{"On", func(w *corazawaf.WAF) bool { return w.RequestBodyAccess }},
+			{"Off", func(w *corazawaf.WAF) bool { return !w.RequestBodyAccess }},
+		},
+		"SecResponseBodyLimitAction": {
+			{"", expectErrorOnDirective},
+			{"What?", expectErrorOnDirective},
+			{"Reject", func(w *corazawaf.WAF) bool { return w.ResponseBodyLimitAction == types.BodyLimitActionReject }},
+			{"ProcessPartial", func(w *corazawaf.WAF) bool { return w.ResponseBodyLimitAction == types.BodyLimitActionProcessPartial }},
+		},
+		"SecResponseBodyAccess": {
+			{"", expectErrorOnDirective},
+			{"What?", expectErrorOnDirective},
+			{"On", func(w *corazawaf.WAF) bool { return w.ResponseBodyAccess }},
+			{"Off", func(w *corazawaf.WAF) bool { return !w.ResponseBodyAccess }},
+		},
+		"SecRemoteRulesFailAction": {
+			{"", expectErrorOnDirective},
+			{"What?", expectErrorOnDirective},
+			{"Abort", func(w *corazawaf.WAF) bool { return w.AbortOnRemoteRulesFail }},
+		},
+		"SecDefaultAction": {
+			{"", expectErrorOnDirective},
+		},
+		"SecAuditLog": {
+			{"", expectErrorOnDirective},
+		},
+	}
+
+	for name, dCases := range directiveCases {
+		t.Run(name, func(t *testing.T) {
+			for _, tCase := range dCases {
+				d := directivesMap[strings.ToLower(name)]
+
+				t.Run(tCase.opts, func(t *testing.T) {
+					waf := corazawaf.NewWAF()
+
+					err := d(&DirectiveOptions{
+						Opts: tCase.opts,
+						WAF:  waf,
+					})
+
+					if tCase.check == nil {
+						if err == nil {
+							t.Error("expected error")
+						}
+					} else {
+						if err != nil {
+							t.Errorf("unexpected error: %s", err.Error())
+						}
+
+						if !tCase.check(waf) {
+							t.Errorf("check failed")
+						}
+					}
+				})
+			}
+		})
+	}
+}
diff --git a/internal/seclang/parser_test.go b/internal/seclang/parser_test.go
index b10789a..c5000cc 100644
--- a/internal/seclang/parser_test.go
+++ b/internal/seclang/parser_test.go
@@ -22,7 +22,7 @@ func TestInterruption(t *testing.T) {
 	waf := coraza.NewWAF()
 	p := NewParser(waf)
 	if err := p.FromString(`SecAction "id:1,deny,log,phase:1"`); err != nil {
-		t.Error("Could not create from string")
+		t.Errorf("Could not create from string: %s", err.Error())
 	}
 	tx := waf.NewTransaction()
 	if tx.ProcessRequestHeaders() == nil {
@@ -39,6 +39,20 @@ func TestDirectivesCaseInsensitive(t *testing.T) {
 	}
 }
 
+func TestInvalidDirective(t *testing.T) {
+	waf := coraza.NewWAF()
+	p := NewParser(waf)
+	err := p.FromString("Unknown Rule")
+	if err == nil {
+		t.Error("expected error")
+	}
+
+	err = p.FromString("SecEngineRule")
+	if err == nil {
+		t.Error("expected error")
+	}
+}
+
 func TestCommentsWithBackticks(t *testing.T) {
 	waf := coraza.NewWAF()
 	p := NewParser(waf)
diff --git a/internal/seclang/rule_parser_test.go b/internal/seclang/rule_parser_test.go
index f2018bb..2445ea7 100644
--- a/internal/seclang/rule_parser_test.go
+++ b/internal/seclang/rule_parser_test.go
@@ -4,6 +4,7 @@
 package seclang
 
 import (
+	"errors"
 	"strings"
 	"testing"
 
@@ -87,8 +88,9 @@ func TestSecRuleInlineVariableNegation(t *testing.T) {
 	err = p.FromString(`
 		SecRule REQUEST_URI|!REQUEST_COOKIES: "abc" "id:9,phase:2"
 	`)
-	if !strings.Contains(err.Error(), "failed to compile rule") {
-		t.Errorf("Error should be failed to compile rule, got %s", err)
+	expectedErr := "failed to compile the directive"
+	if !strings.Contains(err.Error(), expectedErr) {
+		t.Errorf("unexpected error, want %q, got %q", expectedErr, err.Error())
 	}
 }
 
@@ -108,8 +110,9 @@ func TestSecRuleUpdateTargetVariableNegation(t *testing.T) {
 		SecRule REQUEST_URI|REQUEST_COOKIES "abc" "id:8,phase:2"
 		SecRuleUpdateTargetById 8 "!REQUEST_HEADERS:"
 	`)
-	if err.Error() != "unknown variable" {
-		t.Errorf("Error should be unknown variable, got %s", err.Error())
+	expectedErr := errors.New("unknown variable")
+	if errors.Unwrap(err).Error() != expectedErr.Error() {
+		t.Fatalf("unexpexted error, want %q, have %q", expectedErr, errors.Unwrap(err).Error())
 	}
 
 	// Try to update undefined rule
@@ -117,9 +120,9 @@ func TestSecRuleUpdateTargetVariableNegation(t *testing.T) {
 		SecRule REQUEST_URI|REQUEST_COOKIES "abc" "id:9,phase:2"
 		SecRuleUpdateTargetById 99 "!REQUEST_HEADERS:xyz"
 	`)
-	if err.Error() != "cannot create a variable exception for an undefined rule" {
-		t.Error("Error should be cannot create a variable exception for an undefined rule, got ",
-			err)
+	expectedErr = errors.New("cannot create a variable exception for an undefined rule")
+	if errors.Unwrap(err).Error() != expectedErr.Error() {
+		t.Fatalf("unexpected error, want %q, have %q", expectedErr, errors.Unwrap(err).Error())
 	}
 }
 
diff --git a/internal/seclang/rules_test.go b/internal/seclang/rules_test.go
index 3aa70bc..4919f39 100644
--- a/internal/seclang/rules_test.go
+++ b/internal/seclang/rules_test.go
@@ -314,8 +314,9 @@ func TestTxIssue147(t *testing.T) {
 	tx := waf.NewTransaction()
 	// response body access is required
 	tx.ResponseBodyAccess = true
-	// we need a content-type header
+	tx.WAF.ResponseBodyMimeTypes = []string{"text/html"}
 	tx.AddResponseHeader("Content-Type", "text/html")
+
 	if tx.IsResponseBodyProcessable() {
 		if it, _, err := tx.WriteResponseBody([]byte("#!/usr/bin/python")); it != nil || err != nil {
 			t.Error(err)
diff --git a/testing/engine/chains.go b/testing/engine/chains.go
index 31c6e1e..f77f144 100644
--- a/testing/engine/chains.go
+++ b/testing/engine/chains.go
@@ -91,7 +91,7 @@ var _ = profile.RegisterProfile(profile.Profile{
 						Output: profile.ExpectedOutput{
 							TriggeredRules:    []int{1, 21},
 							NonTriggeredRules: []int{20, 2, 1313},
-							LogContains:       "FoundSubChain21 REQUEST_HEADERS:Host in MATCHED_VARS_NAMES:REQUEST_HEADERS:Host",
+							LogContains:       "FoundSubChain21 REQUEST_HEADERS:Host in MATCHED_VARS_NAMES",
 						},
 					},
 				},
@@ -154,7 +154,7 @@ SecRule REQUEST_HEADERS "@rx attack21" \
     "msg:'Sub Chain Rule',\
     logdata:'FoundSubChain21 %{MATCHED_VAR} in %{MATCHED_VAR_NAME}',\
     chain"
-    SecRule MATCHED_VAR_NAME "@rx MATCHED_VARS_NAMES:REQUEST_HEADERS:Host" \
+    SecRule MATCHED_VAR_NAME "@rx MATCHED_VARS_NAMES" \
     "msg:'Sub Sub Chain Rule',\
     logdata:'FoundSubSubChain21 %{MATCHED_VAR} in %{MATCHED_VAR_NAME}',\
     block"
diff --git a/testing/engine/ctl.go b/testing/engine/ctl.go
index 8d70a8d..bbeec8f 100644
--- a/testing/engine/ctl.go
+++ b/testing/engine/ctl.go
@@ -48,7 +48,7 @@ var _ = profile.RegisterProfile(profile.Profile{
 		},
 	},
 	Rules: `
-SecDebugLogLevel 5
+SecDebugLogLevel 9
 SecRequestBodyAccess On
 SecAction "id:1, phase:1, ctl:ruleRemoveByTag=test, \
 	ctl:ruleRemoveById=444, \
diff --git a/testing/engine/match.go b/testing/engine/match.go
index 8e46ad7..4241a40 100644
--- a/testing/engine/match.go
+++ b/testing/engine/match.go
@@ -50,7 +50,7 @@ var _ = profile.RegisterProfile(profile.Profile{
 		},
 	},
 	Rules: `
-SecDebugLogLevel 5
+SecDebugLogLevel 9
 SecRule SERVER_ADDR "! ^127" "id:1, phase:1, log"
 
 SecRule SERVER_PORT "!" "id:2, phase:1, log"
diff --git a/testing/engine_test.go b/testing/engine_test.go
index 68eef74..215d11f 100644
--- a/testing/engine_test.go
+++ b/testing/engine_test.go
@@ -34,7 +34,7 @@ func TestDebug(t *testing.T) {
 	if err := test.RunPhases(); err != nil {
 		t.Error(err)
 	}
-	debug := fmt.Sprintf("%s", test.transaction)
+	debug := fmt.Sprint(test.transaction)
 	expected := []string{
 		"REQUEST_URI: /test",
 		"REQUEST_METHOD: OPTIONS",
diff --git a/waf_test.go b/waf_test.go
index d7e9d90..08d52df 100644
--- a/waf_test.go
+++ b/waf_test.go
@@ -3,44 +3,100 @@
 
 package coraza
 
-import "testing"
+import (
+	"errors"
+	"testing"
+)
 
-func TestNewWAFLimits(t *testing.T) {
+func TestRequestBodyLimit(t *testing.T) {
 	testCases := map[string]struct {
-		expectedErr   string
+		expectedErr   error
 		limit         int
 		inMemoryLimit int
 	}{
 		"empty limit": {
-			expectedErr:   "request body limit should be bigger than 0",
 			limit:         0,
 			inMemoryLimit: 2,
+			expectedErr:   errors.New("request body limit should be bigger than 0"),
 		},
 		"empty memory limit": {
-			expectedErr:   "request body memory limit should be bigger than 0",
 			limit:         2,
 			inMemoryLimit: 0,
+			expectedErr:   errors.New("request body memory limit should be bigger than 0"),
 		},
 		"memory limit bigger than limit": {
 			limit:         5,
 			inMemoryLimit: 9,
-			expectedErr:   "request body limit should be at least the memory limit",
+			expectedErr:   errors.New("request body limit should be at least the memory limit"),
+		},
+		"limit bigger than the hard limit": {
+			limit:       1073741825,
+			expectedErr: errors.New("request body limit should be at most 1GB"),
+		},
+		"right limits": {
+			limit:         100,
+			inMemoryLimit: 50,
+		},
+	}
+
+	for name, tCase := range testCases {
+		t.Run(name, func(t *testing.T) {
+			cfg := NewWAFConfig().(*wafConfig)
+			cfg.requestBodyLimit = &tCase.limit
+			cfg.requestBodyInMemoryLimit = &tCase.inMemoryLimit
+
+			_, err := NewWAF(cfg)
+			if tCase.expectedErr == nil {
+				if err != nil {
+					t.Fatalf("unexpected error: %s", err.Error())
+				}
+			} else {
+				if err == nil {
+					t.Fatal("expected error")
+				}
+
+				if want, have := tCase.expectedErr, err; want.Error() != have.Error() {
+					t.Fatalf("unexpected error: want %q, have %q", want, have)
+				}
+			}
+		})
+	}
+}
+
+func TestResponseBodyLimit(t *testing.T) {
+	testCases := map[string]struct {
+		expectedErr error
+		limit       int
+	}{
+		"empty limit": {
+			limit:       0,
+			expectedErr: errors.New("response body limit should be bigger than 0"),
+		},
+		"limit bigger than the hard limit": {
+			limit:       1073741825,
+			expectedErr: errors.New("response body limit should be at most 1GB"),
+		},
+		"right limit": {
+			limit: 100,
 		},
 	}
 
 	for name, tCase := range testCases {
 		t.Run(name, func(t *testing.T) {
-			_, err := NewWAF(&wafConfig{
-				requestBodyLimit:         tCase.limit,
-				requestBodyInMemoryLimit: tCase.inMemoryLimit,
-			})
+			cfg := NewWAFConfig().(*wafConfig)
+			cfg.responseBodyLimit = &tCase.limit
 
-			if tCase.expectedErr != "" {
+			_, err := NewWAF(cfg)
+			if tCase.expectedErr == nil {
+				if err != nil {
+					t.Fatalf("unexpected error: %s", err.Error())
+				}
+			} else {
 				if err == nil {
 					t.Fatal("expected error")
 				}
 
-				if want, have := tCase.expectedErr, err.Error(); want != have {
+				if want, have := tCase.expectedErr, err; want.Error() != have.Error() {
 					t.Fatalf("unexpected error: want %q, have %q", want, have)
 				}
 			}