Enhance API path normalization and configuration validation
- Introduced a new function to normalize request URL paths, collapsing duplicate slashes and clearing raw paths to ensure correct routing in the reverse proxy. - Updated the gateway to utilize the normalization function for API requests, improving routing consistency. - Trimmed whitespace from server configuration fields in the validation process to prevent potential issues with malformed URLs. - Enhanced documentation in GATEWAY_RUN.md to clarify the importance of proper URL formatting and configuration.
This commit is contained in:
@@ -4,9 +4,32 @@ import (
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// NormalizeRequestURLPath collapses duplicate slashes and dot-segments in req.URL.Path
|
||||
// (path.Clean) and clears RawPath so strip-prefix routing matches the client path even
|
||||
// when the request line contained "//" (e.g. /api//mtg/health). Without this, the path
|
||||
// may not match /api/{alias} and SingleHostReverseProxy forwards a wrong path upstream.
|
||||
func NormalizeRequestURLPath(req *http.Request) {
|
||||
if req == nil || req.URL == nil {
|
||||
return
|
||||
}
|
||||
u := req.URL
|
||||
if u.Path == "" {
|
||||
u.Path = "/"
|
||||
u.RawPath = ""
|
||||
return
|
||||
}
|
||||
c := path.Clean(u.Path)
|
||||
if !strings.HasPrefix(c, "/") {
|
||||
c = "/" + c
|
||||
}
|
||||
u.Path = c
|
||||
u.RawPath = ""
|
||||
}
|
||||
|
||||
// NewReverseProxy builds a reverse proxy to target base URL with path rewriting:
|
||||
// stripPrefix (/api/{alias}) + pathPrefix (/v1) + remainder, joined onto target via url.JoinPath
|
||||
// (e.g. https://host/api/ + v1 + health → https://host/v1/health).
|
||||
@@ -20,6 +43,7 @@ func NewReverseProxy(target *url.URL, stripPrefix, pathPrefix string, setAuth st
|
||||
orig := proxy.Director
|
||||
targetQuery := target.RawQuery
|
||||
proxy.Director = func(req *http.Request) {
|
||||
NormalizeRequestURLPath(req)
|
||||
p := req.URL.Path
|
||||
if !strings.HasPrefix(p, stripPrefix) {
|
||||
orig(req)
|
||||
|
||||
@@ -25,6 +25,32 @@ func (c *captureTransport) RoundTrip(req *http.Request) (*http.Response, error)
|
||||
}, nil
|
||||
}
|
||||
|
||||
func TestDirectorDoubleSlashPathMatchesStripPrefix(t *testing.T) {
|
||||
target, err := url.Parse("http://127.0.0.1:9")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cap := &captureTransport{}
|
||||
rp := NewReverseProxy(target, "/api/mtg", "/v1", "")
|
||||
rp.Transport = cap
|
||||
|
||||
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "http://127.0.0.1:9/", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req.URL.Path = "/api//mtg/health"
|
||||
rp.ServeHTTP(httptest.NewRecorder(), req)
|
||||
|
||||
if cap.got == nil {
|
||||
t.Fatal("no outgoing request captured")
|
||||
}
|
||||
want, err := url.Parse("http://127.0.0.1:9/v1/health")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertSameURL(t, cap.got.URL, want)
|
||||
}
|
||||
|
||||
func TestDirectorRewritesPath(t *testing.T) {
|
||||
target, err := url.Parse("http://127.0.0.1:9")
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user