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 1/2] 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) + } +} From 1243d2d37ad0fae1c255cbfda0efbe97ef7bce62 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Tue, 21 Apr 2026 18:02:57 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20address=20PR=20review=20for=20replac?= =?UTF-8?q?er=20=E2=80=94=20nil=20check,=20EscapedPath,=20scheme=20reuse,?= =?UTF-8?q?=20perf?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - add req.URL nil guard - use EscapedPath for {path} to avoid illegal header characters - reuse reverseProxyRequestScheme for {scheme} consistency - replace strings.NewReplacer with struct fields + strings.ReplaceAll --- reverseproxy.go | 62 +++++++++++++++++++++++++++++-------------------- 1 file changed, 37 insertions(+), 25 deletions(-) diff --git a/reverseproxy.go b/reverseproxy.go index 5bb124f..1cf0078 100644 --- a/reverseproxy.go +++ b/reverseproxy.go @@ -264,32 +264,26 @@ func (ops *HeaderOps) Provision() error { } type reverseProxyReplacer struct { - req *http.Request - repl *strings.Replacer + method, host, path, query, scheme, uri, proto string } func newReverseProxyReplacer(req *http.Request) *reverseProxyReplacer { - 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, - ) + if req == nil || req.URL == nil { + return &reverseProxyReplacer{} + } + uri := req.RequestURI + if uri == "" { + uri = req.URL.RequestURI() + } + return &reverseProxyReplacer{ + method: req.Method, + host: req.Host, + path: req.URL.EscapedPath(), + query: req.URL.RawQuery, + scheme: reverseProxyRequestScheme(req), + uri: uri, + proto: req.Proto, } - return r } func newReverseProxyReplacerFromHeader(hdr http.Header) *reverseProxyReplacer { @@ -300,10 +294,28 @@ func (r *reverseProxyReplacer) Replace(s string) string { if r == nil || s == "" { return s } - if r.repl == nil { - return s + if r.method != "" { + s = strings.ReplaceAll(s, "{method}", r.method) } - return r.repl.Replace(s) + if r.host != "" { + s = strings.ReplaceAll(s, "{host}", r.host) + } + if r.path != "" { + s = strings.ReplaceAll(s, "{path}", r.path) + } + if r.query != "" { + s = strings.ReplaceAll(s, "{query}", r.query) + } + if r.scheme != "" { + s = strings.ReplaceAll(s, "{scheme}", r.scheme) + } + if r.uri != "" { + s = strings.ReplaceAll(s, "{uri}", r.uri) + } + if r.proto != "" { + s = strings.ReplaceAll(s, "{proto}", r.proto) + } + return s } type reverseProxyHandler struct {