diff --git a/modules/caddyhttp/replacer.go b/modules/caddyhttp/replacer.go index 65f9dd475..c510938c1 100644 --- a/modules/caddyhttp/replacer.go +++ b/modules/caddyhttp/replacer.go @@ -57,7 +57,6 @@ func NewTestReplacer(req *http.Request) *caddy.Replacer { func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.ResponseWriter) { SetVar(req.Context(), "start_time", time.Now()) - SetVar(req.Context(), "uuid", new(requestID)) httpVars := func(key string) (any, bool) { if req != nil { @@ -213,8 +212,14 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo return time.Since(start).Seconds() * 1e3, true // multiply seconds to preserve decimal (see #4666) case "http.request.uuid": - // fetch the UUID for this request - id := GetVar(req.Context(), "uuid").(*requestID) + // fetch the UUID for this request, generating and caching it + // on first access so requests that never reference the UUID + // don't pay for the allocation + id, ok := GetVar(req.Context(), "uuid").(*requestID) + if !ok { + id = new(requestID) + SetVar(req.Context(), "uuid", id) + } // set it to this request's access log extra := req.Context().Value(ExtraLogFieldsCtxKey).(*ExtraLogFields) diff --git a/modules/caddyhttp/replacer_test.go b/modules/caddyhttp/replacer_test.go index 4f8d8f0b2..b77b599dc 100644 --- a/modules/caddyhttp/replacer_test.go +++ b/modules/caddyhttp/replacer_test.go @@ -296,3 +296,42 @@ func TestHTTPProtoNameNormalization(t *testing.T) { } } } + +// BenchmarkAddHTTPVarsToReplacer measures the per-request replacer setup, which +// is the common path where the request UUID is never referenced. +func BenchmarkAddHTTPVarsToReplacer(b *testing.B) { + req := httptest.NewRequest(http.MethodGet, "http://example.com/foo?a=b", nil) + req.Header.Set("User-Agent", "test-agent") + ctx := context.WithValue(req.Context(), VarsCtxKey, make(map[string]any)) + ctx = context.WithValue(ctx, ExtraLogFieldsCtxKey, new(ExtraLogFields)) + req = req.WithContext(ctx) + + b.ReportAllocs() + for b.Loop() { + repl := caddy.NewReplacer() + addHTTPVarsToReplacer(repl, req, nil) + } +} + +// TestHTTPVarReplacementUUID verifies the lazily-allocated request UUID is +// generated on first access and stays stable across references. +func TestHTTPVarReplacementUUID(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil) + repl := caddy.NewReplacer() + ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl) + ctx = context.WithValue(ctx, VarsCtxKey, make(map[string]any)) + ctx = context.WithValue(ctx, ExtraLogFieldsCtxKey, new(ExtraLogFields)) + req = req.WithContext(ctx) + addHTTPVarsToReplacer(repl, req, nil) + + first, ok := repl.GetString("http.request.uuid") + if !ok || first == "" { + t.Fatalf("expected a non-empty uuid, got %q (ok=%t)", first, ok) + } + + second, _ := repl.GetString("http.request.uuid") + if first != second { + t.Errorf("expected stable uuid across references: %q != %q", first, second) + } +} +