Add CORS support and response caching to aggregate endpoints
Publish telemt-api gateway Docker image / test (push) Successful in 25s
Publish telemt-api gateway Docker image / build-and-push (push) Successful in 1m19s

- Introduced CORS configuration options in config.example.yaml, allowing specification of allowed origins for cross-origin requests.
- Enhanced the aggregate handler to support response caching with a configurable TTL, improving performance for repeated requests.
- Updated the aggregate API to return a structured response indicating whether any upstream requests failed, enhancing error handling and response clarity.
- Modified documentation in AGGREGATE.md and README.md to reflect the new CORS and caching features.
- Added tests to validate the new functionality in the aggregate handler.
This commit is contained in:
Denozordec
2026-03-30 10:02:16 +07:00
parent 2a8390e687
commit 04c257a84e
14 changed files with 1071 additions and 142 deletions
+51 -1
View File
@@ -28,6 +28,7 @@ type Gateway struct {
log *slog.Logger
transport *http.Transport
promHandler http.Handler
corsAllowed []string
}
// NewGateway builds handlers and reverse proxies from parsed config.
@@ -68,19 +69,68 @@ func NewGateway(p *config.Parsed, log *slog.Logger, geo *geoip.Service) (*Gatewa
}
g.proxies[s.Alias] = rp
}
g.agg = aggregate.NewHandler(p, &http.Client{Transport: t}, geo)
var aggCacheTTL time.Duration
if p.Config.Aggregate != nil && p.Config.Aggregate.CacheTTLMs > 0 {
aggCacheTTL = time.Duration(p.Config.Aggregate.CacheTTLMs) * time.Millisecond
}
g.corsAllowed = append([]string(nil), p.Config.CorsAllowedOrigins...)
g.agg = aggregate.NewHandler(p, &http.Client{Transport: t}, geo, aggCacheTTL)
return g, nil
}
// Handler returns the root HTTP handler with middleware.
func (g *Gateway) Handler() http.Handler {
var h http.Handler = http.HandlerFunc(g.serve)
h = g.withCORS(h)
h = g.withWhitelist(h)
h = g.withAccessLog(h)
h = g.withMetrics(h)
return h
}
func (g *Gateway) withCORS(next http.Handler) http.Handler {
if len(g.corsAllowed) == 0 {
return next
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Vary", "Origin")
origin := r.Header.Get("Origin")
ok, allowOrigin := corsMatch(g.corsAllowed, origin)
if ok {
w.Header().Set("Access-Control-Allow-Origin", allowOrigin)
w.Header().Set("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type, X-Request-Id")
w.Header().Set("Access-Control-Max-Age", "86400")
}
if r.Method == http.MethodOptions {
if ok {
w.WriteHeader(http.StatusNoContent)
return
}
}
next.ServeHTTP(w, r)
})
}
func corsMatch(allowed []string, origin string) (ok bool, allowOrigin string) {
if origin == "" {
return false, ""
}
for _, a := range allowed {
a = strings.TrimSpace(a)
if a == "" {
continue
}
if a == "*" {
return true, "*"
}
if strings.EqualFold(a, origin) {
return true, origin
}
}
return false, ""
}
func (g *Gateway) withWhitelist(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/health" {