package proxy import ( "net/http" "net/http/httputil" "net/url" "strings" "time" "github.com/gin-gonic/gin" ) // Upstream holds a reverse proxy for one backend service. type Upstream struct { Name string rp *httputil.ReverseProxy } // New creates an Upstream reverse proxy pointing at target (e.g. "http://localhost:5010"). func New(name, target string) *Upstream { u, err := url.Parse(target) if err != nil { panic("invalid upstream URL for " + name + ": " + err.Error()) } rp := httputil.NewSingleHostReverseProxy(u) rp.Transport = &http.Transport{ MaxIdleConns: 100, IdleConnTimeout: 90 * time.Second, DisableCompression: false, } // Custom error handler so we return JSON rather than Go's default HTML rp.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusBadGateway) _, _ = w.Write([]byte(`{"code":"bad_gateway","message":"upstream unavailable"}`)) } // Rewrite the request before forwarding orig := rp.Director rp.Director = func(req *http.Request) { orig(req) // Normalise away a trailing slash (gin's RedirectTrailingSlash adds one when a // route is registered as catch-all, e.g. /v1/nodes/*path, but upstream services // register the canonical no-slash form and redirect /nodes/ → /nodes — without // this the two redirects form an infinite loop). if len(req.URL.Path) > 1 && strings.HasSuffix(req.URL.Path, "/") { req.URL.Path = strings.TrimRight(req.URL.Path, "/") } req.Header.Set("X-Forwarded-Host", req.Host) req.Header.Del("X-Forwarded-For") // let httputil re-add it properly req.Host = u.Host } return &Upstream{Name: name, rp: rp} } // Handler returns a gin.HandlerFunc that proxies the request. func (up *Upstream) Handler() gin.HandlerFunc { return func(c *gin.Context) { up.rp.ServeHTTP(c.Writer, c.Request) } } // StripPrefixHandler proxies after stripping a URL prefix (e.g. "/api" → ""). func (up *Upstream) StripPrefixHandler(prefix string) gin.HandlerFunc { return func(c *gin.Context) { c.Request.URL.Path = strings.TrimPrefix(c.Request.URL.Path, prefix) if c.Request.URL.Path == "" { c.Request.URL.Path = "/" } up.rp.ServeHTTP(c.Writer, c.Request) } }