Files
telemt-api/internal/proxy/reverse.go
T
Denozordec 368473de59
Publish telemt-api gateway Docker image / test (push) Successful in 24s
Publish telemt-api gateway Docker image / build-and-push (push) Successful in 1m52s
Enhance API routing and testing for user endpoints
- Updated the reverse proxy to redirect `GET` requests for `/api/{alias}/users` to `/v1/stats/users`, ensuring compatibility with Telemt builds that handle these requests differently.
- Added tests to verify that `GET` and `HEAD` requests to `/api/{alias}/users` are correctly rewritten, while `POST` requests and user-specific retrievals remain unchanged.
- Improved documentation in GATEWAY_RUN.md to clarify the behavior of the API routing and the importance of using the correct base URL.
2026-03-30 11:18:36 +07:00

83 lines
2.4 KiB
Go

package proxy
import (
"net/http"
"net/http/httputil"
"net/url"
"strings"
)
// 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).
//
// Compatibility: GET/HEAD .../api/{alias}/users (list only, no extra path segment) is sent upstream as
// /v1/stats/users. Some Telemt builds treat GET /v1/users incorrectly (e.g. 400) while /v1/stats/users works.
// POST .../users (create) and GET .../users/{username} are unchanged.
func NewReverseProxy(target *url.URL, stripPrefix, pathPrefix string, setAuth string) *httputil.ReverseProxy {
proxy := httputil.NewSingleHostReverseProxy(target)
proxy.Transport = DirectTransport()
orig := proxy.Director
targetQuery := target.RawQuery
proxy.Director = func(req *http.Request) {
p := req.URL.Path
if !strings.HasPrefix(p, stripPrefix) {
orig(req)
if setAuth != "" {
req.Header.Set("Authorization", setAuth)
}
// Server-side requests carry RequestURI; client RoundTrip rejects it with URL.Host set.
req.RequestURI = ""
req.Header.Del("Host")
req.Host = req.URL.Host
return
}
rest := strings.TrimPrefix(p, stripPrefix)
rest = strings.TrimPrefix(rest, "/")
if (req.Method == http.MethodGet || req.Method == http.MethodHead) && rest == "users" {
rest = "stats/users"
}
joined := buildUpstreamURL(target, pathPrefix, rest)
req.URL.Scheme = joined.Scheme
req.URL.Host = joined.Host
req.URL.Path = joined.Path
req.URL.RawPath = joined.RawPath
req.URL.Opaque = ""
// Match Host header to authority; clear stale map entry (e.g. from httptest.NewRequest).
req.Header.Del("Host")
req.Host = req.URL.Host
if targetQuery == "" || req.URL.RawQuery == "" {
req.URL.RawQuery = targetQuery + req.URL.RawQuery
} else {
req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery
}
if setAuth != "" {
req.Header.Set("Authorization", setAuth)
}
req.RequestURI = ""
}
return proxy
}
func buildUpstreamURL(target *url.URL, pathPrefix, rest string) *url.URL {
rel := strings.Trim(pathPrefix, "/")
if rest != "" {
if rel != "" {
rel = rel + "/" + rest
} else {
rel = rest
}
}
var parts []string
for _, seg := range strings.Split(rel, "/") {
if seg != "" {
parts = append(parts, seg)
}
}
if len(parts) == 0 {
out := *target
return &out
}
return target.JoinPath(parts...)
}