Skip to content

Commit ba21576

Browse files
fix(platform-api): align upstreamDefinition timeout and weight validation with the gateway
Restrict an upstreamDefinition connect timeout to ms, s, m, or h units. time.ParseDuration alone also accepts ns/us units and compound values like 1h30m that the gateway rejects at deploy, so without this an API saves at the control plane and then fails to deploy. This matches the connect pattern already published in the OpenAPI spec. Set the upstream weight schema minimum to 0 to match the validator and the gateway, which both accept a 0..100 range.
1 parent a860c44 commit ba21576

3 files changed

Lines changed: 21 additions & 1 deletion

File tree

platform-api/src/internal/service/api.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -673,6 +673,12 @@ func (s *APIService) validateSubscriptionPlans(planNames *[]string, orgUUID stri
673673
// referenceable and deployable at the gateway.
674674
var upstreamRefNameRe = regexp.MustCompile(`^[a-zA-Z0-9\-_]+$`)
675675

676+
// connectTimeoutRe is the contract an upstreamDefinition connect timeout must match: a whole or
677+
// decimal value in ms, s, m, or h. It mirrors the connect pattern published in the OpenAPI spec
678+
// and the gateway validator, since time.ParseDuration alone also accepts ns/us units and compound
679+
// values like "1h30m" that the gateway rejects at deploy.
680+
var connectTimeoutRe = regexp.MustCompile(`^\d+(\.\d+)?(ms|s|m|h)$`)
681+
676682
// validHTTPMethods is the set of operation methods the gateway accepts.
677683
var validHTTPMethods = map[string]bool{
678684
"GET": true, "POST": true, "PUT": true, "DELETE": true, "PATCH": true, "HEAD": true, "OPTIONS": true,
@@ -745,6 +751,9 @@ func (s *APIService) validateUpstreamRefs(upstreamDefs *[]api.ReusableUpstream,
745751
if dur <= 0 {
746752
return fmt.Errorf("upstreamDefinitions %q timeout.connect %q must be positive", d.Name, *d.Timeout.Connect)
747753
}
754+
if !connectTimeoutRe.MatchString(*d.Timeout.Connect) {
755+
return fmt.Errorf("upstreamDefinitions %q timeout.connect %q must use unit ms, s, m, or h (e.g. 5s, 500ms)", d.Name, *d.Timeout.Connect)
756+
}
748757
}
749758
defined[d.Name] = true
750759
}

platform-api/src/internal/service/api_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -676,6 +676,17 @@ func TestValidateUpstreamRefs(t *testing.T) {
676676
t.Errorf("expected nil for valid timeout.connect, got %v", err)
677677
}
678678
})
679+
t.Run("non-canonical timeout.connect unit rejected", func(t *testing.T) {
680+
// time.ParseDuration accepts these, but the gateway only allows ms, s, m, h, so the
681+
// control plane must reject them too or the API saves and then fails to deploy.
682+
for _, v := range []string{"1h30m", "500ns", "500us"} {
683+
d := ru("t")
684+
d.Timeout = &api.UpstreamTimeout{Connect: &v}
685+
if err := s.validateUpstreamRefs(&[]api.ReusableUpstream{d}, api.Upstream{}, nil); err == nil {
686+
t.Errorf("expected error for non-canonical timeout.connect unit %q", v)
687+
}
688+
}
689+
})
679690
t.Run("no refs and no defs is valid", func(t *testing.T) {
680691
if err := s.validateUpstreamRefs(nil, api.Upstream{}, nil); err != nil {
681692
t.Errorf("expected nil, got %v", err)

platform-api/src/resources/openapi.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6556,7 +6556,7 @@ components:
65566556
weight:
65576557
type: integer
65586558
description: Weight for load balancing (optional, default 100)
6559-
minimum: 1
6559+
minimum: 0
65606560
maximum: 100
65616561
example: 80
65626562

0 commit comments

Comments
 (0)