diff --git a/internal/serviceoffercontroller/controller.go b/internal/serviceoffercontroller/controller.go index 20a5714f..81771180 100644 --- a/internal/serviceoffercontroller/controller.go +++ b/internal/serviceoffercontroller/controller.go @@ -747,8 +747,12 @@ func (c *Controller) reconcileUpstream(ctx context.Context, status *monetizeapi. } defer response.Body.Close() - if response.StatusCode >= 500 { - setCondition(status, "UpstreamHealthy", "False", "Unhealthy", fmt.Sprintf("HTTP %d from upstream", response.StatusCode)) + // Require a 2xx health response. Treating any <500 (including 404) as + // healthy left agents with a wrong healthPath (or a never-started API) + // stuck in UpstreamHealthy=True and then Ready=True while paid traffic + // 404'd end-to-end. + if !upstreamHealthStatusOK(response.StatusCode) { + setCondition(status, "UpstreamHealthy", "False", "Unhealthy", fmt.Sprintf("HTTP %d from upstream health path %s", response.StatusCode, offer.EffectiveHealthPath())) return false, nil } @@ -756,6 +760,12 @@ func (c *Controller) reconcileUpstream(ctx context.Context, status *monetizeapi. return true, nil } +// upstreamHealthStatusOK reports whether an HTTP status from the offer's +// healthPath should count as UpstreamHealthy=True. +func upstreamHealthStatusOK(code int) bool { + return code >= 200 && code < 300 +} + func (c *Controller) reconcilePaymentGate(ctx context.Context, status *monetizeapi.ServiceOfferStatus, offer *monetizeapi.ServiceOffer) error { if err := c.applyObject(ctx, c.referenceGrants.Namespace("x402"), buildReferenceGrant(offer)); err != nil { setCondition(status, "PaymentGateReady", "False", "ApplyFailed", err.Error()) diff --git a/internal/serviceoffercontroller/controller_test.go b/internal/serviceoffercontroller/controller_test.go index 8d308b0b..3eb68da5 100644 --- a/internal/serviceoffercontroller/controller_test.go +++ b/internal/serviceoffercontroller/controller_test.go @@ -8,6 +8,28 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) +func TestUpstreamHealthStatusOK(t *testing.T) { + // 2xx only — 404/3xx/5xx must not flip UpstreamHealthy=True. + cases := map[int]bool{ + 200: true, + 201: true, + 204: true, + 301: false, + 302: false, + 400: false, + 401: false, + 404: false, + 500: false, + 503: false, + 0: false, + } + for code, want := range cases { + if got := upstreamHealthStatusOK(code); got != want { + t.Errorf("upstreamHealthStatusOK(%d) = %v, want %v", code, got, want) + } + } +} + func TestSelectRegistrationOwnerPrefersOldestEnabledOffer(t *testing.T) { now := time.Now().UTC() offers := []*monetizeapi.ServiceOffer{