Skip to content

Commit 99bde72

Browse files
committed
feat(state): Improve errors for validation failures
1 parent bc53bd0 commit 99bde72

5 files changed

Lines changed: 145 additions & 60 deletions

File tree

internal/api/state/v1alpha1/server.go

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import (
99
"go.opentelemetry.io/otel/propagation"
1010
"go.uber.org/fx"
1111
"go.uber.org/zap"
12+
"google.golang.org/grpc/codes"
13+
"google.golang.org/grpc/status"
1214
"google.golang.org/protobuf/types/known/timestamppb"
1315
)
1416

@@ -48,7 +50,14 @@ func (s *StateServiceServer) EnsureStore(ctx context.Context, req *statev1alpha1
4850
err := s.state.EnsureStore(ctx, &state.StoreConfig{
4951
Name: req.Store,
5052
})
51-
if err != nil {
53+
54+
if errors.Is(err, context.Canceled) {
55+
return nil, status.Error(codes.Canceled, "context canceled")
56+
} else if errors.Is(err, context.DeadlineExceeded) {
57+
return nil, status.Error(codes.DeadlineExceeded, "timed out")
58+
} else if state.IsValidationError(err) {
59+
return nil, status.Error(codes.InvalidArgument, err.Error())
60+
} else if err != nil {
5261
return nil, err
5362
}
5463

@@ -62,6 +71,12 @@ func (s *StateServiceServer) Get(ctx context.Context, req *statev1alpha1.GetRequ
6271
return &statev1alpha1.GetResponse{
6372
Revision: 0,
6473
}, nil
74+
} else if errors.Is(err, context.Canceled) {
75+
return nil, status.Error(codes.Canceled, "context canceled")
76+
} else if errors.Is(err, context.DeadlineExceeded) {
77+
return nil, status.Error(codes.DeadlineExceeded, "timed out")
78+
} else if state.IsValidationError(err) {
79+
return nil, status.Error(codes.InvalidArgument, err.Error())
6580
} else if err != nil {
6681
return nil, err
6782
}
@@ -84,7 +99,13 @@ func (s *StateServiceServer) Set(ctx context.Context, req *statev1alpha1.SetRequ
8499
revision, err = s.state.Set(ctx, req.Store, req.Key, req.Value)
85100
}
86101

87-
if err != nil {
102+
if errors.Is(err, context.Canceled) {
103+
return nil, status.Error(codes.Canceled, "context canceled")
104+
} else if errors.Is(err, context.DeadlineExceeded) {
105+
return nil, status.Error(codes.DeadlineExceeded, "timed out")
106+
} else if state.IsValidationError(err) {
107+
return nil, status.Error(codes.InvalidArgument, err.Error())
108+
} else if err != nil {
88109
return nil, err
89110
}
90111

@@ -100,7 +121,14 @@ func (s *StateServiceServer) Delete(ctx context.Context, req *statev1alpha1.Dele
100121
} else {
101122
err = s.state.Delete(ctx, req.Store, req.Key)
102123
}
103-
if err != nil {
124+
125+
if errors.Is(err, context.Canceled) {
126+
return nil, status.Error(codes.Canceled, "context canceled")
127+
} else if errors.Is(err, context.DeadlineExceeded) {
128+
return nil, status.Error(codes.DeadlineExceeded, "timed out")
129+
} else if state.IsValidationError(err) {
130+
return nil, status.Error(codes.InvalidArgument, err.Error())
131+
} else if err != nil {
104132
return nil, err
105133
}
106134

internal/state/errors.go

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,8 @@ package state
22

33
import "errors"
44

5-
// ErrStoreRequired is returned when a store is not provided.
6-
var ErrStoreRequired = errors.New("store required")
7-
85
// ErrStoreNotFound is returned when a store is not found.
9-
var ErrStoreNotFound = errors.New("store not found")
10-
11-
// ErrKeyRequired is returned when a key is not provided.
12-
var ErrKeyRequired = errors.New("missing key")
6+
var ErrStoreNotFound = &validationError{err: "store not found"}
137

148
// ErrKeyNotFound is returned when a key is not found in a store.
159
var ErrKeyNotFound = errors.New("key not found")
@@ -21,3 +15,20 @@ var ErrKeyAlreadyExists = errors.New("key already exists")
2115
// ErrRevisionMismatch is returned when a revision does not match the current
2216
// revision of a key.
2317
var ErrRevisionMismatch = errors.New("revision mismatch")
18+
19+
type validationError struct {
20+
err string
21+
}
22+
23+
func (e *validationError) Error() string {
24+
return e.err
25+
}
26+
27+
func newValidationError(err string) error {
28+
return &validationError{err: err}
29+
}
30+
31+
func IsValidationError(err error) bool {
32+
_, ok := err.(*validationError)
33+
return ok
34+
}

internal/state/manager.go

Lines changed: 47 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package state
22

33
import (
44
"context"
5-
"strings"
65
"time"
76

87
"github.com/cockroachdb/errors"
@@ -82,13 +81,12 @@ func (m *Manager) EnsureStore(ctx context.Context, config *StoreConfig) error {
8281
)
8382
defer span.End()
8483

85-
storeName := strings.TrimSpace(config.Name)
86-
if storeName == "" {
87-
span.SetStatus(codes.Error, "store required")
88-
return errors.WithStack(ErrStoreRequired)
84+
if !IsValidStoreName(config.Name) {
85+
span.SetStatus(codes.Error, "invalid store name")
86+
return newValidationError("invalid store name: " + config.Name)
8987
}
9088

91-
_, err := m.stores.Get(ctx, storeName)
89+
_, err := m.stores.Get(ctx, config.Name)
9290
if errors.Is(err, ErrStoreNotFound) {
9391
_, err = m.js.CreateKeyValue(ctx, jetstream.KeyValueConfig{
9492
Bucket: config.Name,
@@ -124,17 +122,10 @@ func (m *Manager) Get(ctx context.Context, store string, key string) (*Entry, er
124122
)
125123
defer span.End()
126124

127-
store = strings.TrimSpace(store)
128-
key = strings.TrimSpace(key)
129-
130-
if store == "" {
131-
span.SetStatus(codes.Error, "store required")
132-
return nil, errors.WithStack(ErrStoreRequired)
133-
}
134-
135-
if key == "" {
136-
span.SetStatus(codes.Error, "key required")
137-
return nil, errors.WithStack(ErrKeyRequired)
125+
err := validatePreconditions(store, key)
126+
if err != nil {
127+
span.SetStatus(codes.Error, err.Error())
128+
return nil, err
138129
}
139130

140131
bucket, err := m.stores.Get(ctx, store)
@@ -187,17 +178,10 @@ func (m *Manager) Create(ctx context.Context, store string, key string, value *a
187178
)
188179
defer span.End()
189180

190-
store = strings.TrimSpace(store)
191-
key = strings.TrimSpace(key)
192-
193-
if store == "" {
194-
span.SetStatus(codes.Error, "store required")
195-
return 0, errors.WithStack(ErrStoreRequired)
196-
}
197-
198-
if key == "" {
199-
span.SetStatus(codes.Error, "key required")
200-
return 0, errors.WithStack(ErrKeyRequired)
181+
err := validatePreconditions(store, key)
182+
if err != nil {
183+
span.SetStatus(codes.Error, err.Error())
184+
return 0, err
201185
}
202186

203187
bucket, err := m.stores.Get(ctx, store)
@@ -244,6 +228,12 @@ func (m *Manager) Set(ctx context.Context, store string, key string, value *anyp
244228
)
245229
defer span.End()
246230

231+
err := validatePreconditions(store, key)
232+
if err != nil {
233+
span.SetStatus(codes.Error, err.Error())
234+
return 0, err
235+
}
236+
247237
bucket, err := m.stores.Get(ctx, store)
248238
if err != nil {
249239
span.RecordError(err)
@@ -286,6 +276,12 @@ func (m *Manager) Update(ctx context.Context, store string, key string, value *a
286276
)
287277
defer span.End()
288278

279+
err := validatePreconditions(store, key)
280+
if err != nil {
281+
span.SetStatus(codes.Error, err.Error())
282+
return 0, err
283+
}
284+
289285
bucket, err := m.stores.Get(ctx, store)
290286
if err != nil {
291287
span.RecordError(err)
@@ -339,6 +335,12 @@ func (m *Manager) Delete(ctx context.Context, store string, key string) error {
339335
)
340336
defer span.End()
341337

338+
err := validatePreconditions(store, key)
339+
if err != nil {
340+
span.SetStatus(codes.Error, err.Error())
341+
return err
342+
}
343+
342344
bucket, err := m.stores.Get(ctx, store)
343345
if err != nil {
344346
span.RecordError(err)
@@ -373,6 +375,12 @@ func (m *Manager) DeleteWithRevision(ctx context.Context, store string, key stri
373375
)
374376
defer span.End()
375377

378+
err := validatePreconditions(store, key)
379+
if err != nil {
380+
span.SetStatus(codes.Error, err.Error())
381+
return err
382+
}
383+
376384
bucket, err := m.stores.Get(ctx, store)
377385
if err != nil {
378386
span.RecordError(err)
@@ -390,3 +398,14 @@ func (m *Manager) DeleteWithRevision(ctx context.Context, store string, key stri
390398
span.SetStatus(codes.Ok, "")
391399
return nil
392400
}
401+
402+
func validatePreconditions(store string, key string) error {
403+
if !IsValidStoreName(store) {
404+
return newValidationError("invalid store name: " + store)
405+
}
406+
407+
if !IsValidKey(key) {
408+
return newValidationError("invalid key: " + key)
409+
}
410+
return nil
411+
}

internal/state/names.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package state
2+
3+
import "windshift/service/internal/events"
4+
5+
// IsValidStoreName checks if the store name is valid.
6+
func IsValidStoreName(name string) bool {
7+
return events.IsValidStreamName(name)
8+
}
9+
10+
// IsValidKey checks if the key name is valid. The NATS documentation says
11+
// that keys follow the same rules as subjects so we delegate to events.IsValidSubject.
12+
func IsValidKey(name string) bool {
13+
return events.IsValidSubject(name, false)
14+
}

0 commit comments

Comments
 (0)