Skip to content

Request & Response Mutators

Middleware does not touch the underlying request/response objects directly; it reads and writes through mutator interfaces. Middleware always receives the full read-write mutator (RequestMutator / ResponseMutator); the read/write grouping below is for readability only and does not represent separate exported interfaces.

text
RequestMutator  =  Read methods  +  Write methods
ResponseMutator =  Read methods  +  Write methods
        ↑                                    ↑
  middleware rewrites the request      middleware reads/rewrites the response
  via RequestMutator                   via ResponseMutator

The Handler signature func(ctx, RequestMutator) (ResponseMutator, error) exposes exactly these two mutators as the middleware's entry and exit points.

Request Mutator

Read methods

The following methods read request data. Call them when middleware only needs to inspect request properties.

MethodReturn typeDescription
Method()stringHTTP method
URL()stringRequest URL
Headers()map[string]stringAll request headers (key -> single value)
QueryParams()map[string]anyQuery parameters
Body()anyRequest body
Timeout()time.DurationRequest timeout
MaxRetries()intMax retry count
Context()context.ContextRequest context
Cookies()[]http.CookieRequest cookies
FollowRedirects()*boolWhether to follow redirects (nil = use default)
MaxRedirects()*intMax redirect count (nil = use default)
StreamBody()boolWhether to stream the request body

Write methods

The following methods modify request data. Call them when middleware only needs to modify request properties.

MethodDescription
SetMethod(string)Set the HTTP method
SetURL(string)Set the URL
SetHeaders(map[string]string)Set all request headers (full replacement)
SetHeader(key, value string)Set a single request header (add/modify)
SetQueryParams(map[string]any)Set query parameters
SetBody(any)Set the request body
SetTimeout(time.Duration)Set the timeout
SetMaxRetries(int)Set the max retry count
SetContext(context.Context)Set the context
SetCookies([]http.Cookie)Set cookies
SetFollowRedirects(*bool)Set whether to follow redirects
SetMaxRedirects(*int)Set the max redirect count
SetStreamBody(bool)Set whether to stream

RequestMutator

RequestMutator is the read-write request mutator interface that httpc exposes, covering every method in the Read methods and Write methods tables above. Its internal read/write sub-interfaces live in the internal/types package and are not exported separately; externally they are referenced uniformly as RequestMutator. Middleware inspects and rewrites request properties through it before the request is sent.

Typical RequestMutator Operations in Middleware

ScenarioMethod combinationDescription
Modify request headersSetHeader(key, val) / Headers() + SetHeaderInject auth headers, trace IDs, API version
Modify query parametersQueryParams() -> add/remove -> SetQueryParamsAppend common query parameters
Modify request bodyBody() -> transform -> SetBodyRequest-body compression, signature injection
Set timeoutSetTimeout(d)Dynamically adjust timeout per request path
Set contextSetContext(ctx)Middleware-level timeout (how TimeoutMiddleware works)
go
// Typical: read existing headers, append a custom header, write back
headers := req.Headers()
headers["X-Trace-ID"] = generateTraceID()
req.SetHeaders(headers)

// Equivalent (more concise)
req.SetHeader("X-Trace-ID", generateTraceID())

Response Mutator

Read methods

The following methods read response data.

MethodReturn typeDescription
StatusCode()intStatus code
Status()stringStatus text (e.g. "200 OK")
Proto()stringProtocol version (e.g. "HTTP/1.1")
Headers()http.HeaderResponse headers
Body()stringResponse body (string)
RawBody()[]byteResponse body (bytes)
ContentLength()int64Content length
Duration()time.DurationRequest duration
Attempts()intAttempt count (including retries)
Cookies()[]*http.CookieResponse cookies
RedirectChain()[]stringRedirect chain (URL of each hop)
RedirectCount()intRedirect count
RequestHeaders()http.HeaderRequest headers actually sent
RequestURL()stringActual request URL (final URL after redirects)
RequestMethod()stringRequest method

Write methods

The following methods modify response data.

MethodDescription
SetStatusCode(int)Set the status code
SetStatus(string)Set the status text
SetProto(string)Set the protocol version
SetHeaders(http.Header)Set response headers (full replacement)
SetBody(string)Set the response body
SetRawBody([]byte)Set the response body (bytes)
SetContentLength(int64)Set the content length
SetDuration(time.Duration)Set the duration
SetAttempts(int)Set the attempt count
SetCookies([]*http.Cookie)Set cookies
SetRedirectChain([]string)Set the redirect chain
SetRedirectCount(int)Set the redirect count
SetRequestHeaders(http.Header)Set request headers
SetRequestURL(string)Set the request URL
SetRequestMethod(string)Set the request method
SetHeader(key string, values ...string)Set a single response header (add/modify)

ResponseMutator

ResponseMutator is the read-write response mutator interface that httpc exposes, covering every method in the Read methods and Write methods tables above. Its internal read/write sub-interfaces live in the internal/types package and are not exported separately; externally they are referenced uniformly as ResponseMutator. Middleware reads or rewrites the response through it after the request completes — commonly used for response caching, content transformation (e.g. JSON pretty-printing), encoding/decoding, and response filtering.

Typical ResponseMutator Operations in Middleware

ScenarioMethod combinationDescription
Read status codeStatusCode()Conditional logging, error classification
Read response headersHeaders()Extract X-Request-ID, Content-Type
Compute metricsDuration() + Attempts()Report duration, retry count
Trace redirectsRedirectChain() + RedirectCount()Audit redirect paths
Modify response headersSetHeader(key, vals...)Append trace headers, security headers

Type Assertion: Accessing Engine-Specific Methods

The RequestMutator received by middleware is at runtime actually of type *engine.Request (the engine's concrete request struct). finalHandler reads three engine-specific hooks not on the interface from it via type assertion. Custom middleware that needs these hooks must do the same.

Interface boundary

The OnRequest/OnResponse callbacks and AllowPrivateIPs are not on the RequestMutator interface — their signatures reference types from the internal engine package (*engine.Request/*engine.Response), and exposing them on the public interface would cause an import cycle. Therefore they can only be accessed via *engine.Request type assertion.

These engine-specific methods include:

Method (only on *engine.Request)Description
OnRequest() func(*engine.Request) errorCallback fired before the request is sent
OnResponse() func(*engine.Response) errorCallback fired after the response is received
AllowPrivateIPs() *boolPer-request SSRF override
SetOnRequest(func) / SetOnResponse(func)Set callbacks
SetAllowPrivateIPs(*bool)Set the SSRF override

The vast majority of middleware does not need type assertion — the RequestMutator/ResponseMutator interfaces already cover all common read/write operations. You only need to assert to the concrete type when you need callbacks or SSRF overrides.

SanitizedURL Cache

Multiple middlewares may all need to log the sanitized URL (a URL with credentials removed). To avoid recomputing it, HTTPC caches the sanitized result on the request object so it is shared across middlewares for the same request.

text
getOrComputeSanitizedURL(req):
  ① Does req implement the sanitizedURLer interface (SanitizedURL/SetSanitizedURL)?
     - *engine.Request implements this interface
  ② Cached? -> return the cached value directly
  ③ Not cached? -> compute SanitizeURL(req.URL()), cache it, return it

The built-in LoggingMiddleware, MetricsMiddleware, and AuditMiddleware all use getOrComputeSanitizedURL to share the sanitized result, so URL sanitization is computed only once across the entire chain. Custom middleware that logs URLs should use this mechanism too, rather than calling req.URL() directly (which may contain credentials).

URL sanitization

When logging URLs in a logging/metrics middleware, never use req.URL() directly — if the URL contains user:pass@host credentials, they will leak into the logs. The built-in middlewares automatically strip the credentials via getOrComputeSanitizedURL.

Example: Reading and Writing via Mutators

An authentication middleware that injects an auth header via RequestMutator.SetHeader and reads the response status code via ResponseMutator.StatusCode.

go
package main

import (
	"context"
	"fmt"

	"github.com/cybergodev/httpc"
)

// authMiddleware injects an auth header via RequestMutator
// and reads the status code via ResponseMutator
func authMiddleware(token string) httpc.MiddlewareFunc {
	return func(next httpc.Handler) httpc.Handler {
		return func(ctx context.Context, req httpc.RequestMutator) (httpc.ResponseMutator, error) {
			// Write: set a request header via RequestMutator
			req.SetHeader("Authorization", "Bearer "+token)
			// Read: inspect the request method via RequestMutator
			fmt.Printf("Sending %s request\n", req.Method())

			resp, err := next(ctx, req)
			if err != nil {
				return nil, err
			}
			// Read: get the status code via ResponseMutator
			fmt.Printf("Received status code %d\n", resp.StatusCode())
			return resp, nil
		}
	}
}

func main() {
	cfg := httpc.DefaultConfig()
	cfg.Middleware.Middlewares = []httpc.MiddlewareFunc{
		authMiddleware("my-secret-token"),
	}
	client, err := httpc.New(cfg)
	if err != nil {
		panic(err)
	}
	defer client.Close()

	result, err := client.Get("https://httpbin.org/get")
	if err != nil {
		panic(err)
	}
	fmt.Println(result.IsSuccess())
	// Sample output:
	// Sending GET request
	// Received status code 200
	// true
}

Practical Example: Request/Response Logging Middleware

A complete logging middleware that demonstrates the read/write capabilities of both RequestMutator and ResponseMutator — reading the request method/URL and response status code/duration/retry info via mutators and formatting the output uniformly.

go
package main

import (
	"context"
	"fmt"
	"log"
	"time"

	"github.com/cybergodev/httpc"
)

// loggingMiddleware reads full request and response info via mutators and formats the output
func loggingMiddleware() httpc.MiddlewareFunc {
	return func(next httpc.Handler) httpc.Handler {
		return func(ctx context.Context, req httpc.RequestMutator) (httpc.ResponseMutator, error) {
			start := time.Now()

			// Request phase: read request info
			log.Printf("[REQ] %s %s", req.Method(), req.URL())

			resp, err := next(ctx, req)
			duration := time.Since(start)

			if err != nil {
				// Error response: no status code to read
				log.Printf("[ERR] %s %s -> %v (%v)",
					req.Method(), req.URL(), err, duration)
				return nil, err
			}

			// Response phase: read status code, duration, retry count, redirect chain
			log.Printf("[RESP] %s %s -> %d (%v, attempts=%d, redirects=%d)",
				req.Method(),
				req.URL(),
				resp.StatusCode(),
				duration,
				resp.Attempts(),
				resp.RedirectCount(),
			)
			return resp, nil
		}
	}
}

func main() {
	cfg := httpc.DefaultConfig()
	cfg.Middleware.Middlewares = []httpc.MiddlewareFunc{
		loggingMiddleware(),
	}
	client, err := httpc.New(cfg)
	if err != nil {
		panic(err)
	}
	defer client.Close()

	result, err := client.Get("https://httpbin.org/get")
	if err != nil {
		panic(err)
	}
	fmt.Println("status code:", result.StatusCode())
	// Sample output:
	// [REQ] GET https://httpbin.org/get
	// [RESP] GET https://httpbin.org/get -> 200 (123.456ms, attempts=1, redirects=0)
	// status code: 200
}

See Also