Skip to content

Commit c4102c4

Browse files
committed
Refactor error handling logic.
1 parent a5bd752 commit c4102c4

6 files changed

Lines changed: 63 additions & 47 deletions

File tree

transport/http/controllers/APIController.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ func NewAPIController(ctr *container.Container) *APIController {
2020
}
2121

2222
// GetInfo return basic details of the API.
23-
func (ctl *APIController) GetInfo(w http.ResponseWriter, r *http.Request) {
23+
func (ctl *APIController) GetInfo(w http.ResponseWriter, r *http.Request) error {
2424
// transform
2525
tr := transformers.APITransformer{
2626
Name: "StoryBuilder",
@@ -29,4 +29,5 @@ func (ctl *APIController) GetInfo(w http.ResponseWriter, r *http.Request) {
2929
}
3030
// send response
3131
ctl.sendResponse(r.Context(), w, http.StatusOK, tr)
32+
return nil
3233
}

transport/http/controllers/Controller.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,17 @@ func (ctl *Controller) sendResponse(_ context.Context, w http.ResponseWriter, co
4545
func (ctl *Controller) sendError(ctx context.Context, w http.ResponseWriter, err any) {
4646
response.Error(ctx, w, err, ctl.logger)
4747
}
48+
49+
// Action represents an HTTP handler that can return an error.
50+
type Action func(w http.ResponseWriter, r *http.Request) error
51+
52+
// Wrap takes an Action and converts it to a standard http.HandlerFunc.
53+
// If the Action returns an error, Wrap will automatically send the error response.
54+
func (ctl *Controller) Wrap(action Action) http.HandlerFunc {
55+
return func(w http.ResponseWriter, r *http.Request) {
56+
if err := action(w, r); err != nil {
57+
// use the request context to ensure tracing works
58+
ctl.sendError(r.Context(), w, err)
59+
}
60+
}
61+
}

transport/http/controllers/SampleController.go

Lines changed: 27 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/storybuilder/storybuilder/transport/http/request/unpackers"
1414
"github.com/storybuilder/storybuilder/transport/http/response"
1515
"github.com/storybuilder/storybuilder/transport/http/response/transformers"
16+
"github.com/storybuilder/storybuilder/transport/http/errors"
1617
)
1718

1819
// SampleController contains controller logic for endpoints.
@@ -30,29 +31,28 @@ func NewSampleController(ctr *container.Container) *SampleController {
3031
}
3132

3233
// Get handles retrieving a list of samples.
33-
func (ctl *SampleController) Get(w http.ResponseWriter, r *http.Request) {
34+
func (ctl *SampleController) Get(w http.ResponseWriter, r *http.Request) error {
3435
// get the context
3536
ctx := r.Context()
3637
// add a trace string to the context
3738
ctx = ctl.withTrace(ctx, "SampleController.Get")
3839
// get data
3940
samples, err := ctl.sampleUseCase.Get(ctx)
4041
if err != nil {
41-
ctl.sendError(ctx, w, err)
42-
return
42+
return err
4343
}
4444
// transform
4545
tr, err := response.Transform(samples, transformers.NewSampleTransformer(), true)
4646
if err != nil {
47-
ctl.sendError(ctx, w, err)
48-
return
47+
return err
4948
}
5049
// send response
5150
ctl.sendResponse(ctx, w, http.StatusOK, tr)
51+
return nil
5252
}
5353

5454
// GetByID handles retrieving a single sample.
55-
func (ctl *SampleController) GetByID(w http.ResponseWriter, r *http.Request) {
55+
func (ctl *SampleController) GetByID(w http.ResponseWriter, r *http.Request) error {
5656
// get the context
5757
ctx := r.Context()
5858
// add a trace string to the context
@@ -63,27 +63,25 @@ func (ctl *SampleController) GetByID(w http.ResponseWriter, r *http.Request) {
6363
// validate
6464
errs := ctl.validator.ValidateField(id, "required,gt=0")
6565
if errs != nil {
66-
ctl.sendError(ctx, w, errs)
67-
return
66+
return errors.ValidationMapError(errs)
6867
}
6968
// get data
7069
smpl, err := ctl.sampleUseCase.GetByID(ctx, id)
7170
if err != nil {
72-
ctl.sendError(ctx, w, err)
73-
return
71+
return err
7472
}
7573
// transform
7674
tr, err := response.Transform(smpl, transformers.NewSampleTransformer(), false)
7775
if err != nil {
78-
ctl.sendError(ctx, w, err)
79-
return
76+
return err
8077
}
8178
// send response
8279
ctl.sendResponse(ctx, w, http.StatusOK, tr)
80+
return nil
8381
}
8482

8583
// Add adds a new sample entry.
86-
func (ctl *SampleController) Add(w http.ResponseWriter, r *http.Request) {
84+
func (ctl *SampleController) Add(w http.ResponseWriter, r *http.Request) error {
8785
// get the context
8886
ctx := r.Context()
8987
// add a trace string to the context
@@ -92,14 +90,12 @@ func (ctl *SampleController) Add(w http.ResponseWriter, r *http.Request) {
9290
sampleUnpacker := unpackers.NewSampleUnpacker()
9391
err := request.Unpack(r, sampleUnpacker)
9492
if err != nil {
95-
ctl.sendError(ctx, w, err)
96-
return
93+
return err
9794
}
9895
// validate unpacked data
9996
errs := ctl.validator.Validate(sampleUnpacker)
10097
if errs != nil {
101-
ctl.sendError(ctx, w, errs)
102-
return
98+
return errors.ValidationMapError(errs)
10399
}
104100
// bind unpacked data to entities
105101
smpl := entities.Sample{
@@ -109,17 +105,17 @@ func (ctl *SampleController) Add(w http.ResponseWriter, r *http.Request) {
109105
// add
110106
err = ctl.sampleUseCase.Add(ctx, smpl)
111107
if err != nil {
112-
ctl.sendError(ctx, w, err)
113-
return
108+
return err
114109
}
115110
// transform
116111
// tr := response.Transform(sample, transformers.NewSampleTransformer(), false)
117112
// send response
118113
ctl.sendResponse(ctx, w, http.StatusCreated)
114+
return nil
119115
}
120116

121117
// Edit updates an existing sample entry.
122-
func (ctl *SampleController) Edit(w http.ResponseWriter, r *http.Request) {
118+
func (ctl *SampleController) Edit(w http.ResponseWriter, r *http.Request) error {
123119
// get the context
124120
ctx := r.Context()
125121
// add a trace string to the context
@@ -128,23 +124,20 @@ func (ctl *SampleController) Edit(w http.ResponseWriter, r *http.Request) {
128124
sampleUnpacker := unpackers.NewSampleUnpacker()
129125
err := request.Unpack(r, sampleUnpacker)
130126
if err != nil {
131-
ctl.sendError(ctx, w, err)
132-
return
127+
return err
133128
}
134129
// get id from request
135-
idVal := ctx.Value("id").(string)
130+
idVal := chi.URLParam(r, "id")
136131
id, _ := strconv.Atoi(idVal)
137132
// validate request parameters
138133
errs := ctl.validator.ValidateField(id, "required,gt=0")
139134
if errs != nil {
140-
ctl.sendError(ctx, w, errs)
141-
return
135+
return errors.ValidationMapError(errs)
142136
}
143137
// validate unpacked data
144138
errs = ctl.validator.Validate(sampleUnpacker)
145139
if errs != nil {
146-
ctl.sendError(ctx, w, errs)
147-
return
140+
return errors.ValidationMapError(errs)
148141
}
149142
// bind unpacked data to entities
150143
smpl := entities.Sample{
@@ -155,34 +148,33 @@ func (ctl *SampleController) Edit(w http.ResponseWriter, r *http.Request) {
155148
// edit
156149
err = ctl.sampleUseCase.Edit(ctx, smpl)
157150
if err != nil {
158-
ctl.sendError(ctx, w, err)
159-
return
151+
return err
160152
}
161153
// send response
162154
ctl.sendResponse(ctx, w, http.StatusNoContent)
155+
return nil
163156
}
164157

165158
// Delete deletes an existing sample entry.
166-
func (ctl *SampleController) Delete(w http.ResponseWriter, r *http.Request) {
159+
func (ctl *SampleController) Delete(w http.ResponseWriter, r *http.Request) error {
167160
// get the context
168161
ctx := r.Context()
169162
// add a trace string to the context
170163
ctx = ctl.withTrace(ctx, "SampleController.Delete")
171164
// get id from request
172-
idVal := ctx.Value("id").(string)
165+
idVal := chi.URLParam(r, "id")
173166
id, _ := strconv.Atoi(idVal)
174167
// validate request parameters
175168
errs := ctl.validator.ValidateField(id, "required,gt=0")
176169
if errs != nil {
177-
ctl.sendError(ctx, w, errs)
178-
return
170+
return errors.ValidationMapError(errs)
179171
}
180172
// delete
181173
err := ctl.sampleUseCase.Delete(ctx, id)
182174
if err != nil {
183-
ctl.sendError(ctx, w, err)
184-
return
175+
return err
185176
}
186177
// send response
187178
ctl.sendResponse(ctx, w, http.StatusNoContent)
179+
return nil
188180
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package errors
2+
3+
// ValidationMapError is the type of errors thrown by the field wise validator.
4+
type ValidationMapError map[string]string
5+
6+
// Error returns a generic validation failed message.
7+
func (e ValidationMapError) Error() string {
8+
return "validation failed"
9+
}

transport/http/response/responder.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"net/http"
99

1010
errHandler "github.com/storybuilder/storybuilder/transport/http/error"
11+
httpErrs "github.com/storybuilder/storybuilder/transport/http/errors"
1112
"github.com/storybuilder/storybuilder/transport/http/response/mappers"
1213
)
1314

@@ -37,12 +38,11 @@ func Error(ctx context.Context, w http.ResponseWriter, err any, logger *slog.Log
3738
code := http.StatusInternalServerError
3839
// check whether err is a general error or a validation error
3940
errG, isG := err.(error)
40-
errV, isV := err.(map[string]string)
41-
if isG {
42-
msg, code = errHandler.Handle(ctx, errG, logger)
43-
}
41+
errV, isV := err.(httpErrs.ValidationMapError)
4442
if isV {
45-
msg, code = errHandler.HandleValidationErrors(ctx, errV, logger)
43+
msg, code = errHandler.HandleValidationErrors(ctx, map[string]string(errV), logger)
44+
} else if isG {
45+
msg, code = errHandler.Handle(ctx, errG, logger)
4646
}
4747
Send(w, msg, code)
4848
}

transport/http/router/router.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,12 @@ func Init(ctr *container.Container) *chi.Mux {
1919
sampleController := controllers.NewSampleController(ctr)
2020
// bind controller functions to routes
2121
// api info
22-
r.Get("/", apiController.GetInfo)
22+
r.Get("/", apiController.Wrap(apiController.GetInfo))
2323
// sample
24-
r.Get("/samples", sampleController.Get)
25-
r.Get("/samples/{id:[0-9]+}", sampleController.GetByID)
26-
r.Post("/samples", sampleController.Add)
27-
r.Put("/samples/{id:[0-9]+}", sampleController.Edit)
28-
r.Delete("/samples/{id:[0-9]+}", sampleController.Delete)
24+
r.Get("/samples", sampleController.Wrap(sampleController.Get))
25+
r.Get("/samples/{id:[0-9]+}", sampleController.Wrap(sampleController.GetByID))
26+
r.Post("/samples", sampleController.Wrap(sampleController.Add))
27+
r.Put("/samples/{id:[0-9]+}", sampleController.Wrap(sampleController.Edit))
28+
r.Delete("/samples/{id:[0-9]+}", sampleController.Wrap(sampleController.Delete))
2929
return r
3030
}

0 commit comments

Comments
 (0)