Repository: corazawaf/coraza
Pull request #605 (2023-01-30 23:46:55)

fix: fixes the superflous header write.
This PR fixes

```
2023/01/30 22:44:14 http: superfluous response.WriteHeader call from github.com/corazawaf/coraza/v3/http.wrap.func1 (interceptor.go:126)
```

sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=coraza&pullRequest=605)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=coraza&pullRequest=605&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=coraza&pullRequest=605&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=coraza&pullRequest=605&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=coraza&pullRequest=605&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=coraza&pullRequest=605&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=coraza&pullRequest=605&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=coraza&pullRequest=605&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=coraza&pullRequest=605&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=coraza&pullRequest=605&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=coraza&pullRequest=605&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=coraza&pullRequest=605&resolved=false&types=CODE_SMELL) [99 Code Smells](https://sonarcloud.io/project/issues?id=coraza&pullRequest=605&resolved=false&types=CODE_SMELL)

[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/0-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=coraza&pullRequest=605&metric=new_coverage&view=list) [0.0% Coverage](https://sonarcloud.io/component_measures?id=coraza&pullRequest=605&metric=new_coverage&view=list)  
[![0.1%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.1%')](https://sonarcloud.io/component_measures?id=coraza&pullRequest=605&metric=new_duplicated_lines_density&view=list) [0.1% Duplication](https://sonarcloud.io/component_measures?id=coraza&pullRequest=605&metric=new_duplicated_lines_density&view=list)


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=coraza&pullRequest=605)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=coraza&pullRequest=605&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=coraza&pullRequest=605&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=coraza&pullRequest=605&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=coraza&pullRequest=605&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=coraza&pullRequest=605&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=coraza&pullRequest=605&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=coraza&pullRequest=605&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=coraza&pullRequest=605&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=coraza&pullRequest=605&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=coraza&pullRequest=605&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=coraza&pullRequest=605&resolved=false&types=CODE_SMELL) [99 Code Smells](https://sonarcloud.io/project/issues?id=coraza&pullRequest=605&resolved=false&types=CODE_SMELL)

[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/0-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=coraza&pullRequest=605&metric=new_coverage&view=list) [0.0% Coverage](https://sonarcloud.io/component_measures?id=coraza&pullRequest=605&metric=new_coverage&view=list)  
[![0.1%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.1%')](https://sonarcloud.io/component_measures?id=coraza&pullRequest=605&metric=new_duplicated_lines_density&view=list) [0.1% Duplication](https://sonarcloud.io/component_measures?id=coraza&pullRequest=605&metric=new_duplicated_lines_density&view=list)


codecov-commenter: # [Codecov](https://codecov.io/gh/corazawaf/coraza/pull/605?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf) Report
Base: **77.58**% // Head: **77.58**% // Decreases project coverage by **`-0.01%`** :warning:
> Coverage data is based on head [(`e9449df`)](https://codecov.io/gh/corazawaf/coraza/pull/605?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf) compared to base [(`603ed5a`)](https://codecov.io/gh/corazawaf/coraza/commit/603ed5a7c5859554737805a0ba07f29a5477906a?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf).
> Patch coverage: 88.23% of modified lines in pull request are covered.

:mega: This organization is not using Codecov’s [GitHub App Integration](https://github.com/apps/codecov). We recommend you install it so Codecov can continue to function properly for your repositories. [Learn more](https://about.codecov.io/blog/codecov-is-updating-its-github-integration/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf)

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           v3/dev     #605      +/-   ##
==========================================
- Coverage   77.58%   77.58%   -0.01%     
==========================================
  Files         144      144              
  Lines        7481     7489       +8     
==========================================
+ Hits         5804     5810       +6     
- Misses       1446     1448       +2     
  Partials      231      231              
```

| Flag | Coverage Δ | |
|---|---|---|
| default | `72.97% <52.94%> (-0.03%)` | :arrow_down: |
| examples | `28.29% <76.47%> (-0.08%)` | :arrow_down: |
| ftw | `54.62% <35.29%> (-0.05%)` | :arrow_down: |
| ftw-multiphase | `54.72% <35.29%> (-0.05%)` | :arrow_down: |
| tinygo | `71.13% <ø> (ø)` | |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/corazawaf/coraza/pull/605?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf) | Coverage Δ | |
|---|---|---|
| [http/interceptor.go](https://codecov.io/gh/corazawaf/coraza/pull/605?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf#diff-aHR0cC9pbnRlcmNlcHRvci5nbw==) | `48.66% <88.23%> (+1.17%)` | :arrow_up: |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf)

</details>

[:umbrella: View full report at Codecov](https://codecov.io/gh/corazawaf/coraza/pull/605?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf).


Patch:
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..7b53051 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()
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 14de634..8443ea5 100644
--- a/internal/corazawaf/transaction.go
+++ b/internal/corazawaf/transaction.go
@@ -340,14 +340,15 @@ func (tx *Transaction) AddRequestHeader(key string, value string) {
 	tx.variables.requestHeadersNames.AddUniqueCS(keyl, key, keyl)
 	tx.variables.requestHeaders.AddCS(keyl, 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 {
@@ -631,7 +632,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)
@@ -742,6 +742,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
diff --git a/macro/macro.go b/macro/macro.go
index f328699..11d7fbf 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,10 +59,10 @@ 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) {
+	switch col := tx.Collection(token.variable).(type) {
 	case *collection.Map:
 		if c := col.Get(token.key); len(c) > 0 {
 			return c[0]
@@ -100,7 +100,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 +121,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 +137,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/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.


Test patch:
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/http/middleware_test.go b/http/middleware_test.go
index b8c88bd..b4c2b2a 100644
--- a/http/middleware_test.go
+++ b/http/middleware_test.go
@@ -197,35 +197,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 +253,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 +263,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,6 +298,10 @@ 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
@@ -294,7 +309,7 @@ func TestHttpServer(t *testing.T) {
 	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 +482,82 @@ func TestObtainStatusCodeFromInterruptionOrDefault(t *testing.T) {
 		})
 	}
 }
+
+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, _ := coraza.NewWAF(coraza.NewWAFConfig().WithRequestBodyLimit(3))
+	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/corazawaf/transaction_test.go b/internal/corazawaf/transaction_test.go
index 3e13ba4..3c42612 100644
--- a/internal/corazawaf/transaction_test.go
+++ b/internal/corazawaf/transaction_test.go
@@ -946,6 +946,28 @@ func TestTxProcessConnection(t *testing.T) {
 	}
 }
 
+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.String() != "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()