feat: implement Goong API proxy with Redis caching, automatic key injection, and response filtering
All checks were successful
Build and Release / release (push) Successful in 1m31s
All checks were successful
Build and Release / release (push) Successful in 1m31s
This commit is contained in:
@@ -86,7 +86,25 @@ func (ctrl *goongController) Proxy(c fiber.Ctx) error {
|
|||||||
c.Set("Vary", "Origin")
|
c.Set("Vary", "Origin")
|
||||||
c.Set("Cross-Origin-Resource-Policy", "cross-origin")
|
c.Set("Cross-Origin-Resource-Policy", "cross-origin")
|
||||||
|
|
||||||
if c.Method() == "GET" {
|
if c.Method() == "GET" && statusCode == fiber.StatusOK {
|
||||||
|
isStaticAsset := strings.Contains(targetURL, "/tiles/") ||
|
||||||
|
strings.Contains(targetURL, "/fonts/") ||
|
||||||
|
strings.Contains(targetURL, "/glyphs/") ||
|
||||||
|
strings.HasSuffix(targetURL, ".pbf") ||
|
||||||
|
strings.HasSuffix(targetURL, ".png") ||
|
||||||
|
strings.HasSuffix(targetURL, ".jpg") ||
|
||||||
|
strings.HasSuffix(targetURL, ".webp")
|
||||||
|
|
||||||
|
if !isStaticAsset {
|
||||||
|
c.Set("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0")
|
||||||
|
c.Set("CDN-Cache-Control", "no-store")
|
||||||
|
c.Set("Cloudflare-CDN-Cache-Control", "no-store")
|
||||||
|
} else {
|
||||||
|
c.Set("Cache-Control", "public, max-age=86400")
|
||||||
|
c.Set("CDN-Cache-Control", "max-age=86400")
|
||||||
|
c.Set("Cloudflare-CDN-Cache-Control", "max-age=86400")
|
||||||
|
}
|
||||||
|
} else if c.Method() == "GET" {
|
||||||
c.Set("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0")
|
c.Set("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0")
|
||||||
c.Set("CDN-Cache-Control", "no-store")
|
c.Set("CDN-Cache-Control", "no-store")
|
||||||
c.Set("Cloudflare-CDN-Cache-Control", "no-store")
|
c.Set("Cloudflare-CDN-Cache-Control", "no-store")
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
type GoongService interface {
|
type GoongService interface {
|
||||||
@@ -100,18 +102,82 @@ func (s *goongService) ProxyRequest(ctx context.Context, method string, targetUR
|
|||||||
if lowerK == "cookie" || lowerK == "authorization" || lowerK == "x-api-key" || lowerK == "x-csrf-token" {
|
if lowerK == "cookie" || lowerK == "authorization" || lowerK == "x-api-key" || lowerK == "x-csrf-token" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if strings.HasPrefix(lowerK, "sec-") || lowerK == "user-agent" || lowerK == "accept-language" || lowerK == "priority" || lowerK == "purpose" || lowerK == "dnt" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
req.Header.Set(k, v)
|
req.Header.Set(k, v)
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := s.httpClient.Do(req)
|
var resp *http.Response
|
||||||
if err != nil {
|
var lastErr error
|
||||||
return 500, nil, nil, fmt.Errorf("failed to fetch from target: %w", err)
|
var respBody []byte
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
respBody, err := io.ReadAll(resp.Body)
|
for attempt := 1; attempt <= 3; attempt++ {
|
||||||
if err != nil {
|
if ctx.Err() != nil {
|
||||||
return 500, nil, nil, fmt.Errorf("failed to read response body: %w", err)
|
lastErr = ctx.Err()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
attemptReq := req.Clone(ctx)
|
||||||
|
if len(reqBody) > 0 {
|
||||||
|
attemptReq.Body = io.NopCloser(bytes.NewReader(reqBody))
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err = s.httpClient.Do(attemptReq)
|
||||||
|
if err != nil {
|
||||||
|
lastErr = err
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
time.Sleep(time.Duration(attempt*150) * time.Millisecond)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
respBody, err = io.ReadAll(resp.Body)
|
||||||
|
resp.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
lastErr = err
|
||||||
|
time.Sleep(time.Duration(attempt*150) * time.Millisecond)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode == http.StatusOK {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode == http.StatusTooManyRequests || resp.StatusCode >= 500 {
|
||||||
|
lastErr = fmt.Errorf("target returned status code %d: %s", resp.StatusCode, string(respBody))
|
||||||
|
time.Sleep(time.Duration(attempt*150) * time.Millisecond)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp == nil || (resp.StatusCode != http.StatusOK && lastErr != nil) {
|
||||||
|
statusCode := 500
|
||||||
|
if resp != nil {
|
||||||
|
statusCode = resp.StatusCode
|
||||||
|
}
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return 499, nil, nil, ctx.Err()
|
||||||
|
}
|
||||||
|
log.Error().
|
||||||
|
Err(lastErr).
|
||||||
|
Int("status_code", statusCode).
|
||||||
|
Str("url", finalURL).
|
||||||
|
Str("method", method).
|
||||||
|
Msg("Goong Map proxy request failed after retries")
|
||||||
|
return statusCode, nil, nil, fmt.Errorf("failed to fetch from target: %w", lastErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
log.Warn().
|
||||||
|
Int("status_code", resp.StatusCode).
|
||||||
|
Str("url", finalURL).
|
||||||
|
Str("method", method).
|
||||||
|
Str("resp_body", string(respBody)).
|
||||||
|
Msg("Goong Map proxy request returned non-200 status code")
|
||||||
}
|
}
|
||||||
|
|
||||||
respHeaders := make(map[string]string)
|
respHeaders := make(map[string]string)
|
||||||
|
|||||||
Reference in New Issue
Block a user