diff --git a/pkg/provider/proxy.go b/pkg/provider/proxy.go index ccc1db7..c588b32 100644 --- a/pkg/provider/proxy.go +++ b/pkg/provider/proxy.go @@ -8,6 +8,7 @@ import ( "net/url" "strings" + coreapi "dappco.re/go/core/api" "github.com/gin-gonic/gin" ) @@ -39,14 +40,20 @@ type ProxyConfig struct { type ProxyProvider struct { config ProxyConfig proxy *httputil.ReverseProxy + err error } // NewProxy creates a ProxyProvider from the given configuration. -// The upstream URL must be valid or NewProxy will panic. +// Invalid upstream URLs do not panic; the provider retains the +// configuration error and responds with a standard 500 envelope when +// mounted. This keeps provider construction safe for callers. func NewProxy(cfg ProxyConfig) *ProxyProvider { target, err := url.Parse(cfg.Upstream) if err != nil { - panic("provider.NewProxy: invalid upstream URL: " + err.Error()) + return &ProxyProvider{ + config: cfg, + err: err, + } } proxy := httputil.NewSingleHostReverseProxy(target) @@ -71,6 +78,15 @@ func NewProxy(cfg ProxyConfig) *ProxyProvider { } } +// Err reports any configuration error detected while constructing the proxy. +// A nil error means the proxy is ready to mount and serve requests. +func (p *ProxyProvider) Err() error { + if p == nil { + return nil + } + return p.err +} + // stripBasePath removes an exact base path prefix from a request path. // It only strips when the path matches the base path itself or lives under // the base path boundary, so "/api" will not accidentally trim "/api-v2". @@ -112,6 +128,19 @@ func (p *ProxyProvider) BasePath() string { // RegisterRoutes mounts a catch-all reverse proxy handler on the router group. func (p *ProxyProvider) RegisterRoutes(rg *gin.RouterGroup) { rg.Any("/*path", func(c *gin.Context) { + if p == nil || p.err != nil || p.proxy == nil { + details := map[string]any{} + if p != nil && p.err != nil { + details["error"] = p.err.Error() + } + c.JSON(http.StatusInternalServerError, coreapi.FailWithDetails( + "invalid_provider_configuration", + "Provider is misconfigured", + details, + )) + return + } + // Use the underlying http.ResponseWriter directly. Gin's // responseWriter wrapper does not implement http.CloseNotifier, // which httputil.ReverseProxy requires for cancellation signalling. diff --git a/pkg/provider/proxy_test.go b/pkg/provider/proxy_test.go index c1bc536..5f15253 100644 --- a/pkg/provider/proxy_test.go +++ b/pkg/provider/proxy_test.go @@ -183,11 +183,32 @@ func TestProxyProvider_Renderable_Good(t *testing.T) { } func TestProxyProvider_Ugly_InvalidUpstream(t *testing.T) { - assert.Panics(t, func() { - provider.NewProxy(provider.ProxyConfig{ - Name: "bad", - BasePath: "/api/v1/bad", - Upstream: "://not-a-url", - }) + p := provider.NewProxy(provider.ProxyConfig{ + Name: "bad", + BasePath: "/api/v1/bad", + Upstream: "://not-a-url", }) + + require.NotNil(t, p) + assert.Error(t, p.Err()) + + engine, err := api.New() + require.NoError(t, err) + engine.Register(p) + + handler := engine.Handler() + + req := httptest.NewRequest(http.MethodGet, "/api/v1/bad/items", nil) + w := httptest.NewRecorder() + handler.ServeHTTP(w, req) + + assert.Equal(t, http.StatusInternalServerError, w.Code) + + var body map[string]any + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body)) + + assert.Equal(t, false, body["success"]) + errObj, ok := body["error"].(map[string]any) + require.True(t, ok) + assert.Equal(t, "invalid_provider_configuration", errObj["code"]) }