• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

go-fuego / fuego / 13650839440

04 Mar 2025 09:39AM UTC coverage: 90.626% (-0.1%) from 90.724%
13650839440

Pull #431

github

EwenQuim
feat: Unify HTTP errors and improve error messages
Pull Request #431: feat: Unify HTTP errors and improve error messages

47 of 55 new or added lines in 2 files covered. (85.45%)

1 existing line in 1 file now uncovered.

2620 of 2891 relevant lines covered (90.63%)

1.03 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.46
/errors.go
1
package fuego
2

3
import (
4
        "errors"
5
        "log/slog"
6
        "net/http"
7
        "strconv"
8
        "strings"
9
)
10

11
// ErrorWithStatus is an interface that can be implemented by an error to provide
12
// a status code
13
type ErrorWithStatus interface {
14
        error
15
        StatusCode() int
16
}
17

18
// ErrorWithDetail is an interface that can be implemented by an error to provide
19
// an additional detail message about the error
20
type ErrorWithDetail interface {
21
        error
22
        DetailMsg() string
23
}
24

25
// HTTPError is the error response used by the serialization part of the framework.
26
type HTTPError struct {
27
        // Developer readable error message. Not shown to the user to avoid security leaks.
28
        Err error `json:"-" xml:"-" yaml:"-"`
29
        // URL of the error type. Can be used to lookup the error in a documentation
30
        Type string `json:"type,omitempty" xml:"type,omitempty" yaml:"type,omitempty" description:"URL of the error type. Can be used to lookup the error in a documentation"`
31
        // Short title of the error
32
        Title string `json:"title,omitempty" xml:"title,omitempty" yaml:"title,omitempty" description:"Short title of the error"`
33
        // HTTP status code. If using a different type than [HTTPError], for example [BadRequestError], this will be automatically overridden after Fuego error handling.
34
        Status int `json:"status,omitempty" xml:"status,omitempty" yaml:"status,omitempty" description:"HTTP status code" example:"403"`
35
        // Human readable error message
36
        Detail   string      `json:"detail,omitempty" xml:"detail,omitempty" yaml:"detail,omitempty" description:"Human readable error message"`
37
        Instance string      `json:"instance,omitempty" xml:"instance,omitempty" yaml:"instance,omitempty"`
38
        Errors   []ErrorItem `json:"errors,omitempty" xml:"errors,omitempty" yaml:"errors,omitempty"`
39
}
40

41
type ErrorItem struct {
42
        More   map[string]any `json:"more,omitempty" xml:"more,omitempty" description:"Additional information about the error"`
43
        Name   string         `json:"name" xml:"name" description:"For example, name of the parameter that caused the error"`
44
        Reason string         `json:"reason" xml:"reason" description:"Human readable error message"`
45
}
46

47
// PublicError returns a human readable error message.
48
// It ignores the underlying error for security and only returns the status code, title and detail.
49
func (e HTTPError) PublicError() string {
1✔
50
        var msgBuilder strings.Builder
1✔
51

1✔
52
        code := e.StatusCode()
1✔
53
        msgBuilder.WriteString(strconv.Itoa(code))
1✔
54

1✔
55
        title := e.Title
1✔
56
        if title == "" {
2✔
57
                title = http.StatusText(code)
1✔
58
                if title == "" {
1✔
59
                        title = "HTTP Error"
×
60
                }
×
61
        }
62
        msgBuilder.WriteString(" ")
1✔
63
        msgBuilder.WriteString(title)
1✔
64

1✔
65
        if e.Detail != "" {
2✔
66
                msgBuilder.WriteString(" (")
1✔
67
                msgBuilder.WriteString(e.Detail)
1✔
68
                msgBuilder.WriteString(")")
1✔
69
        }
1✔
70

71
        return msgBuilder.String()
1✔
72
}
73

74
func (e HTTPError) Error() string {
1✔
75
        msg := e.PublicError()
1✔
76

1✔
77
        if e.Err != nil {
2✔
78
                msg = msg + ": " + e.Err.Error()
1✔
79
        }
1✔
80

81
        return msg
1✔
82
}
83

84
func (e HTTPError) StatusCode() int {
1✔
85
        if e.Status == 0 {
2✔
86
                return http.StatusInternalServerError
1✔
87
        }
1✔
88
        return e.Status
1✔
89
}
90

91
func (e HTTPError) DetailMsg() string {
1✔
92
        return e.Detail
1✔
93
}
1✔
94

95
func (e HTTPError) Unwrap() error { return e.Err }
1✔
96

97
// BadRequestError is an error used to return a 400 status code.
98
type BadRequestError HTTPError
99

100
var _ ErrorWithStatus = BadRequestError{}
101

NEW
102
func (e BadRequestError) Error() string {
×
NEW
103
        e.Status = http.StatusBadRequest
×
NEW
104
        return HTTPError(e).Error()
×
NEW
105
}
×
106

107
func (e BadRequestError) StatusCode() int { return http.StatusBadRequest }
1✔
108

109
func (e BadRequestError) Unwrap() error { return HTTPError(e) }
1✔
110

111
// NotFoundError is an error used to return a 404 status code.
112
type NotFoundError HTTPError
113

114
var _ ErrorWithStatus = NotFoundError{}
115

116
func (e NotFoundError) Error() string {
1✔
117
        e.Status = http.StatusNotFound
1✔
118
        return HTTPError(e).Error()
1✔
119
}
1✔
120

121
func (e NotFoundError) StatusCode() int { return http.StatusNotFound }
1✔
122

123
func (e NotFoundError) Unwrap() error { return HTTPError(e) }
1✔
124

125
// UnauthorizedError is an error used to return a 401 status code.
126
type UnauthorizedError HTTPError
127

128
var _ ErrorWithStatus = UnauthorizedError{}
129

130
func (e UnauthorizedError) Error() string {
1✔
131
        e.Status = http.StatusUnauthorized
1✔
132
        return HTTPError(e).Error()
1✔
133
}
1✔
134

135
func (e UnauthorizedError) StatusCode() int { return http.StatusUnauthorized }
1✔
136

137
func (e UnauthorizedError) Unwrap() error { return HTTPError(e) }
1✔
138

139
// ForbiddenError is an error used to return a 403 status code.
140
type ForbiddenError HTTPError
141

142
var _ ErrorWithStatus = ForbiddenError{}
143

144
func (e ForbiddenError) Error() string {
1✔
145
        e.Status = http.StatusForbidden
1✔
146
        return HTTPError(e).Error()
1✔
147
}
1✔
148

149
func (e ForbiddenError) StatusCode() int { return http.StatusForbidden }
1✔
150

151
func (e ForbiddenError) Unwrap() error { return HTTPError(e) }
1✔
152

153
// ConflictError is an error used to return a 409 status code.
154
type ConflictError HTTPError
155

156
var _ ErrorWithStatus = ConflictError{}
157

158
func (e ConflictError) Error() string {
1✔
159
        e.Status = http.StatusConflict
1✔
160
        return HTTPError(e).Error()
1✔
161
}
1✔
162

163
func (e ConflictError) StatusCode() int { return http.StatusConflict }
1✔
164

165
func (e ConflictError) Unwrap() error { return HTTPError(e) }
1✔
166

167
// InternalServerError is an error used to return a 500 status code.
168
type InternalServerError = HTTPError
169

170
// NotAcceptableError is an error used to return a 406 status code.
171
type NotAcceptableError HTTPError
172

173
var _ ErrorWithStatus = NotAcceptableError{}
174

NEW
175
func (e NotAcceptableError) Error() string {
×
NEW
176
        e.Status = http.StatusNotAcceptable
×
NEW
177
        return HTTPError(e).Error()
×
NEW
178
}
×
179

180
func (e NotAcceptableError) StatusCode() int { return http.StatusNotAcceptable }
×
181

182
func (e NotAcceptableError) Unwrap() error { return HTTPError(e) }
×
183

184
// ErrorHandler is the default error handler used by the framework.
185
// If the error is an [HTTPError] that error is returned.
186
// If the error adheres to the [ErrorWithStatus] interface
187
// the error is transformed to a [HTTPError] using [HandleHTTPError].
188
// If the error is not an [HTTPError] nor does it adhere to an
189
// interface the error is returned as is.
190
func ErrorHandler(err error) error {
1✔
191
        var errorStatus ErrorWithStatus
1✔
192
        switch {
1✔
193
        case errors.As(err, &HTTPError{}),
194
                errors.As(err, &errorStatus):
1✔
195
                return HandleHTTPError(err)
1✔
196
        }
197

198
        slog.Error("Error in controller", "error", err.Error())
1✔
199

1✔
200
        return err
1✔
201
}
202

203
// HandleHTTPError is the core logic
204
// of handling fuego [HTTPError]'s. This
205
// function takes any error and coerces it into a fuego HTTPError.
206
// This can be used override the default handler:
207
//
208
//        engine := fuego.NewEngine(
209
//                WithErrorHandler(HandleHTTPError),
210
//        )
211
//
212
// or
213
//
214
//        server := fuego.NewServer(
215
//                fuego.WithEngineOptions(
216
//                        fuego.WithErrorHandler(HandleHTTPError),
217
//                ),
218
//        )
219
func HandleHTTPError(err error) error {
1✔
220
        errResponse := HTTPError{
1✔
221
                Err: err,
1✔
222
        }
1✔
223

1✔
224
        var errorInfo HTTPError
1✔
225
        if errors.As(err, &errorInfo) {
2✔
226
                errResponse = errorInfo
1✔
227
        }
1✔
228

229
        // Check status code
230
        var errorStatus ErrorWithStatus
1✔
231
        if errors.As(err, &errorStatus) {
2✔
232
                errResponse.Status = errorStatus.StatusCode()
1✔
233
        }
1✔
234

235
        // Check for detail
236
        var errorDetail ErrorWithDetail
1✔
237
        if errors.As(err, &errorDetail) {
2✔
238
                errResponse.Detail = errorDetail.DetailMsg()
1✔
239
        }
1✔
240

241
        if errResponse.Title == "" {
2✔
242
                errResponse.Title = http.StatusText(errResponse.Status)
1✔
243
        }
1✔
244

245
        slog.Error("Error "+errResponse.Title, "status", errResponse.StatusCode(), "detail", errResponse.DetailMsg(), "error", errResponse.Err)
1✔
246

1✔
247
        return errResponse
1✔
248
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc