From fa925582d7121795671cdcd6e92383d7acf951ee Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Tue, 21 Apr 2026 17:36:38 +0800 Subject: [PATCH] feat: implement dynamic request variable replacement in replacer Replace the no-op reverseProxyReplacer.Replace with strings.NewReplacer supporting {method}, {host}, {path}, {query}, {scheme}, {uri}, {proto} --- reverseproxy.go | 30 +++++++- reverseproxy_headers_replace_test.go | 102 +++++++++++++++++++++++++++ 2 files changed, 129 insertions(+), 3 deletions(-) diff --git a/reverseproxy.go b/reverseproxy.go index 30fa370..5bb124f 100644 --- a/reverseproxy.go +++ b/reverseproxy.go @@ -264,11 +264,32 @@ func (ops *HeaderOps) Provision() error { } type reverseProxyReplacer struct { - req *http.Request + req *http.Request + repl *strings.Replacer } func newReverseProxyReplacer(req *http.Request) *reverseProxyReplacer { - return &reverseProxyReplacer{req: req} + r := &reverseProxyReplacer{req: req} + if req != nil { + uri := req.RequestURI + if uri == "" { + uri = req.URL.RequestURI() + } + scheme := "http" + if req.TLS != nil { + scheme = "https" + } + r.repl = strings.NewReplacer( + "{method}", req.Method, + "{host}", req.Host, + "{path}", req.URL.Path, + "{query}", req.URL.RawQuery, + "{scheme}", scheme, + "{uri}", uri, + "{proto}", req.Proto, + ) + } + return r } func newReverseProxyReplacerFromHeader(hdr http.Header) *reverseProxyReplacer { @@ -279,7 +300,10 @@ func (r *reverseProxyReplacer) Replace(s string) string { if r == nil || s == "" { return s } - return s + if r.repl == nil { + return s + } + return r.repl.Replace(s) } type reverseProxyHandler struct { diff --git a/reverseproxy_headers_replace_test.go b/reverseproxy_headers_replace_test.go index 1eb0d04..0c0d599 100644 --- a/reverseproxy_headers_replace_test.go +++ b/reverseproxy_headers_replace_test.go @@ -426,3 +426,105 @@ func BenchmarkReplacementApplyRegexp(b *testing.B) { _ = r.apply(s) } } + +func TestReverseProxyReplacerDynamicVars(t *testing.T) { + req, _ := http.NewRequest(http.MethodGet, "http://example.com/api/v1/users?sort=name&limit=10", nil) + req.Host = "example.com" + repl := newReverseProxyReplacer(req) + + tests := []struct { + name string + input string + want string + }{ + {"method", "{method}", "GET"}, + {"host", "{host}", "example.com"}, + {"path", "{path}", "/api/v1/users"}, + {"query", "{query}", "sort=name&limit=10"}, + {"scheme", "{scheme}", "http"}, + {"proto", "{proto}", "HTTP/1.1"}, + {"combined", "X-{method}-{path}", "X-GET-/api/v1/users"}, + {"no vars", "static-value", "static-value"}, + {"empty", "", ""}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := repl.Replace(tt.input); got != tt.want { + t.Errorf("Replace(%q) = %q, want %q", tt.input, got, tt.want) + } + }) + } +} + +func TestReverseProxyReplacerNilRequest(t *testing.T) { + repl := newReverseProxyReplacer(nil) + if got := repl.Replace("{method}"); got != "{method}" { + t.Errorf("expected unchanged string with nil request, got %q", got) + } +} + +func TestReverseProxyReplacerNilReplacer(t *testing.T) { + var repl *reverseProxyReplacer + if got := repl.Replace("{method}"); got != "{method}" { + t.Errorf("expected unchanged string with nil replacer, got %q", got) + } +} + +func TestReverseProxyReplacerFromHeader(t *testing.T) { + hdr := make(http.Header) + repl := newReverseProxyReplacerFromHeader(hdr) + if got := repl.Replace("{method}"); got != "{method}" { + t.Errorf("expected unchanged string from header replacer, got %q", got) + } +} + +func TestReverseProxyHeaderOpsWithDynamicVars(t *testing.T) { + backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if got := r.Header.Get("X-Forwarded-Path"); got != "/dynamic/path" { + t.Errorf("expected X-Forwarded-Path=/dynamic/path, got %q", got) + } + if got := r.Header.Get("X-Forwarded-Method"); got != "GET" { + t.Errorf("expected X-Forwarded-Method=GET, got %q", got) + } + if got := r.Header.Get("X-Forwarded-Host"); got != "client.example" { + t.Errorf("expected X-Forwarded-Host=client.example, got %q", got) + } + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("ok")) + })) + defer backend.Close() + + target, err := url.Parse(backend.URL) + if err != nil { + t.Fatalf("parse target: %v", err) + } + + engine := New() + engine.GET("/dynamic/path", ReverseProxy(ReverseProxyConfig{ + Target: target, + RequestHeaders: &HeaderOps{ + Add: map[string][]string{ + "X-Forwarded-Path": {"{path}"}, + "X-Forwarded-Method": {"{method}"}, + "X-Forwarded-Host": {"{host}"}, + }, + }, + })) + + proxy := httptest.NewServer(engine) + defer proxy.Close() + + req, _ := http.NewRequest(http.MethodGet, proxy.URL+"/dynamic/path", nil) + req.Host = "client.example" + resp, err := http.DefaultClient.Do(req) + if err != nil { + t.Fatalf("request failed: %v", err) + } + defer resp.Body.Close() + _, _ = io.ReadAll(resp.Body) + + if resp.StatusCode != http.StatusOK { + t.Errorf("expected status 200, got %d", resp.StatusCode) + } +}