mirror of
https://github.com/sablierapp/sablier.git
synced 2025-12-25 14:59:16 +01:00
fix: add http error responses (#494)
This features adds rfc7807 Problem detail responses when an error happens processing a request. This will greatly improve the common issues with "blank pages" and "404 pages" issues which should now properly tell the user what input was wrong (group that does not exist, container name that does not exist, etc.)
This commit is contained in:
31
app/sessions/errors.go
Normal file
31
app/sessions/errors.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package sessions
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ErrGroupNotFound struct {
|
||||
Group string
|
||||
AvailableGroups []string
|
||||
}
|
||||
|
||||
func (g ErrGroupNotFound) Error() string {
|
||||
return fmt.Sprintf("group %s not found", g.Group)
|
||||
}
|
||||
|
||||
type ErrRequestBinding struct {
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e ErrRequestBinding) Error() string {
|
||||
return e.Err.Error()
|
||||
}
|
||||
|
||||
type ErrTimeout struct {
|
||||
Duration time.Duration
|
||||
}
|
||||
|
||||
func (e ErrTimeout) Error() string {
|
||||
return fmt.Sprintf("timeout after %s", e.Duration)
|
||||
}
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"maps"
|
||||
"slices"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -17,9 +19,11 @@ import (
|
||||
|
||||
const defaultRefreshFrequency = 2 * time.Second
|
||||
|
||||
//go:generate mockgen -package sessionstest -source=sessions_manager.go -destination=sessionstest/mocks_sessions_manager.go *
|
||||
|
||||
type Manager interface {
|
||||
RequestSession(names []string, duration time.Duration) *SessionState
|
||||
RequestSessionGroup(group string, duration time.Duration) *SessionState
|
||||
RequestSession(names []string, duration time.Duration) (*SessionState, error)
|
||||
RequestSessionGroup(group string, duration time.Duration) (*SessionState, error)
|
||||
RequestReadySession(ctx context.Context, names []string, duration time.Duration, timeout time.Duration) (*SessionState, error)
|
||||
RequestReadySessionGroup(ctx context.Context, group string, duration time.Duration, timeout time.Duration) (*SessionState, error)
|
||||
|
||||
@@ -112,6 +116,10 @@ type SessionState struct {
|
||||
func (s *SessionState) IsReady() bool {
|
||||
ready := true
|
||||
|
||||
if s.Instances == nil {
|
||||
s.Instances = &sync.Map{}
|
||||
}
|
||||
|
||||
s.Instances.Range(func(key, value interface{}) bool {
|
||||
state := value.(InstanceState)
|
||||
if state.Error != nil || state.Instance.Status != instance.Ready {
|
||||
@@ -132,10 +140,9 @@ func (s *SessionState) Status() string {
|
||||
return "not-ready"
|
||||
}
|
||||
|
||||
func (s *SessionsManager) RequestSession(names []string, duration time.Duration) (sessionState *SessionState) {
|
||||
|
||||
func (s *SessionsManager) RequestSession(names []string, duration time.Duration) (sessionState *SessionState, err error) {
|
||||
if len(names) == 0 {
|
||||
return nil
|
||||
return nil, fmt.Errorf("names cannot be empty")
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
@@ -160,19 +167,24 @@ func (s *SessionsManager) RequestSession(names []string, duration time.Duration)
|
||||
|
||||
wg.Wait()
|
||||
|
||||
return sessionState
|
||||
return sessionState, nil
|
||||
}
|
||||
|
||||
func (s *SessionsManager) RequestSessionGroup(group string, duration time.Duration) (sessionState *SessionState) {
|
||||
|
||||
func (s *SessionsManager) RequestSessionGroup(group string, duration time.Duration) (sessionState *SessionState, err error) {
|
||||
if len(group) == 0 {
|
||||
return nil
|
||||
return nil, fmt.Errorf("group is mandatory")
|
||||
}
|
||||
|
||||
names := s.groups[group]
|
||||
names, ok := s.groups[group]
|
||||
if !ok {
|
||||
return nil, ErrGroupNotFound{
|
||||
Group: group,
|
||||
AvailableGroups: slices.Collect(maps.Keys(s.groups)),
|
||||
}
|
||||
}
|
||||
|
||||
if len(names) == 0 {
|
||||
return nil
|
||||
return nil, fmt.Errorf("group has no member")
|
||||
}
|
||||
|
||||
return s.RequestSession(names, duration)
|
||||
@@ -227,8 +239,11 @@ func (s *SessionsManager) requestSessionInstance(name string, duration time.Dura
|
||||
}
|
||||
|
||||
func (s *SessionsManager) RequestReadySession(ctx context.Context, names []string, duration time.Duration, timeout time.Duration) (*SessionState, error) {
|
||||
session, err := s.RequestSession(names, duration)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
session := s.RequestSession(names, duration)
|
||||
if session.IsReady() {
|
||||
return session, nil
|
||||
}
|
||||
@@ -241,7 +256,10 @@ func (s *SessionsManager) RequestReadySession(ctx context.Context, names []strin
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
session := s.RequestSession(names, duration)
|
||||
session, err := s.RequestSession(names, duration)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if session.IsReady() {
|
||||
readiness <- session
|
||||
}
|
||||
@@ -272,7 +290,13 @@ func (s *SessionsManager) RequestReadySessionGroup(ctx context.Context, group st
|
||||
return nil, fmt.Errorf("group is mandatory")
|
||||
}
|
||||
|
||||
names := s.groups[group]
|
||||
names, ok := s.groups[group]
|
||||
if !ok {
|
||||
return nil, ErrGroupNotFound{
|
||||
Group: group,
|
||||
AvailableGroups: slices.Collect(maps.Keys(s.groups)),
|
||||
}
|
||||
}
|
||||
|
||||
if len(names) == 0 {
|
||||
return nil, fmt.Errorf("group has no member")
|
||||
|
||||
144
app/sessions/sessionstest/mocks_sessions_manager.go
Normal file
144
app/sessions/sessionstest/mocks_sessions_manager.go
Normal file
@@ -0,0 +1,144 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: sessions_manager.go
|
||||
//
|
||||
// Generated by this command:
|
||||
//
|
||||
// mockgen -package sessionstest -source=sessions_manager.go -destination=sessionstest/mocks_sessions_manager.go *
|
||||
//
|
||||
|
||||
// Package sessionstest is a generated GoMock package.
|
||||
package sessionstest
|
||||
|
||||
import (
|
||||
context "context"
|
||||
io "io"
|
||||
reflect "reflect"
|
||||
time "time"
|
||||
|
||||
sessions "github.com/sablierapp/sablier/app/sessions"
|
||||
gomock "go.uber.org/mock/gomock"
|
||||
)
|
||||
|
||||
// MockManager is a mock of Manager interface.
|
||||
type MockManager struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockManagerMockRecorder
|
||||
isgomock struct{}
|
||||
}
|
||||
|
||||
// MockManagerMockRecorder is the mock recorder for MockManager.
|
||||
type MockManagerMockRecorder struct {
|
||||
mock *MockManager
|
||||
}
|
||||
|
||||
// NewMockManager creates a new mock instance.
|
||||
func NewMockManager(ctrl *gomock.Controller) *MockManager {
|
||||
mock := &MockManager{ctrl: ctrl}
|
||||
mock.recorder = &MockManagerMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockManager) EXPECT() *MockManagerMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// LoadSessions mocks base method.
|
||||
func (m *MockManager) LoadSessions(arg0 io.ReadCloser) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "LoadSessions", arg0)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// LoadSessions indicates an expected call of LoadSessions.
|
||||
func (mr *MockManagerMockRecorder) LoadSessions(arg0 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadSessions", reflect.TypeOf((*MockManager)(nil).LoadSessions), arg0)
|
||||
}
|
||||
|
||||
// RequestReadySession mocks base method.
|
||||
func (m *MockManager) RequestReadySession(ctx context.Context, names []string, duration, timeout time.Duration) (*sessions.SessionState, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "RequestReadySession", ctx, names, duration, timeout)
|
||||
ret0, _ := ret[0].(*sessions.SessionState)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// RequestReadySession indicates an expected call of RequestReadySession.
|
||||
func (mr *MockManagerMockRecorder) RequestReadySession(ctx, names, duration, timeout any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RequestReadySession", reflect.TypeOf((*MockManager)(nil).RequestReadySession), ctx, names, duration, timeout)
|
||||
}
|
||||
|
||||
// RequestReadySessionGroup mocks base method.
|
||||
func (m *MockManager) RequestReadySessionGroup(ctx context.Context, group string, duration, timeout time.Duration) (*sessions.SessionState, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "RequestReadySessionGroup", ctx, group, duration, timeout)
|
||||
ret0, _ := ret[0].(*sessions.SessionState)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// RequestReadySessionGroup indicates an expected call of RequestReadySessionGroup.
|
||||
func (mr *MockManagerMockRecorder) RequestReadySessionGroup(ctx, group, duration, timeout any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RequestReadySessionGroup", reflect.TypeOf((*MockManager)(nil).RequestReadySessionGroup), ctx, group, duration, timeout)
|
||||
}
|
||||
|
||||
// RequestSession mocks base method.
|
||||
func (m *MockManager) RequestSession(names []string, duration time.Duration) (*sessions.SessionState, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "RequestSession", names, duration)
|
||||
ret0, _ := ret[0].(*sessions.SessionState)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// RequestSession indicates an expected call of RequestSession.
|
||||
func (mr *MockManagerMockRecorder) RequestSession(names, duration any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RequestSession", reflect.TypeOf((*MockManager)(nil).RequestSession), names, duration)
|
||||
}
|
||||
|
||||
// RequestSessionGroup mocks base method.
|
||||
func (m *MockManager) RequestSessionGroup(group string, duration time.Duration) (*sessions.SessionState, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "RequestSessionGroup", group, duration)
|
||||
ret0, _ := ret[0].(*sessions.SessionState)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// RequestSessionGroup indicates an expected call of RequestSessionGroup.
|
||||
func (mr *MockManagerMockRecorder) RequestSessionGroup(group, duration any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RequestSessionGroup", reflect.TypeOf((*MockManager)(nil).RequestSessionGroup), group, duration)
|
||||
}
|
||||
|
||||
// SaveSessions mocks base method.
|
||||
func (m *MockManager) SaveSessions(arg0 io.WriteCloser) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "SaveSessions", arg0)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// SaveSessions indicates an expected call of SaveSessions.
|
||||
func (mr *MockManagerMockRecorder) SaveSessions(arg0 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveSessions", reflect.TypeOf((*MockManager)(nil).SaveSessions), arg0)
|
||||
}
|
||||
|
||||
// Stop mocks base method.
|
||||
func (m *MockManager) Stop() {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "Stop")
|
||||
}
|
||||
|
||||
// Stop indicates an expected call of Stop.
|
||||
func (mr *MockManagerMockRecorder) Stop() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Stop", reflect.TypeOf((*MockManager)(nil).Stop))
|
||||
}
|
||||
Reference in New Issue
Block a user