Enhance API path normalization and configuration validation
Publish telemt-api gateway Docker image / test (push) Successful in 25s
Publish telemt-api gateway Docker image / build-and-push (push) Successful in 2m14s

- 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:
Denozordec
2026-03-30 11:29:58 +07:00
parent 368473de59
commit a52dff4944
5 changed files with 59 additions and 0 deletions
+4
View File
@@ -75,6 +75,10 @@ func (c *Config) Validate() error {
seen := make(map[string]struct{})
for i := range c.Servers {
s := &c.Servers[i]
s.Alias = strings.TrimSpace(s.Alias)
s.BaseURL = strings.TrimSpace(s.BaseURL)
s.PathPrefix = strings.TrimSpace(s.PathPrefix)
s.AuthorizationEnv = strings.TrimSpace(s.AuthorizationEnv)
if s.Alias == "" {
return fmt.Errorf("servers[%d]: alias is required", i)
}
+24
View File
@@ -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)
+26
View File
@@ -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 {
+4
View File
@@ -228,6 +228,10 @@ func (g *Gateway) serve(w http.ResponseWriter, r *http.Request) {
g.promHandler.ServeHTTP(w, r)
return
}
// So /api//mtg/health and /api/../api/agg/... route like /api/mtg/health and /api/agg/...
if strings.HasPrefix(r.URL.Path, "/api") {
proxy.NormalizeRequestURLPath(r)
}
const prefix = "/api/"
if r.URL.Path == "/api/agg" || strings.HasPrefix(r.URL.Path, "/api/agg/") {
g.agg.ServeHTTP(w, r)