File size: 9,690 Bytes
04f1444
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
package apierrors

import (
	"context"
	"fmt"
	"net/http"
	"strings"

	"github.com/go-chi/chi/v5/middleware"
)

const (
	// Content Type
	ContentTypeKey          = "Content-Type"
	ContentTypeProblemValue = "application/problem+json"

	// Internal
	InternalType   = "https://kongapi.info/konnect/internal"
	InternalTitle  = "Internal"
	InternalDetail = "An internal failure occurred"

	// Service Unavailable
	UnavailableType   = "https://kongapi.info/konnect/unavailable"
	UnavailableTitle  = "Unavailable"
	UnavailableDetail = "The requested service is unavailable"

	// NotImplemented
	NotImplementedType   = "https://kongapi.info/konnect/not-implemented"
	NotImplementedTitle  = "Not Implemented"
	NotImplementedDetail = "The requested functionality is not implemented"

	// Unauthenticated
	UnauthenticatedType   = "https://kongapi.info/konnect/unauthenticated"
	UnauthenticatedTitle  = "Unauthenticated"
	UnauthenticatedDetail = "A valid token is required"

	// Forbidden
	ForbiddenType   = "https://kongapi.info/konnect/unauthorized"
	ForbiddenTitle  = "Forbidden"
	ForbiddenDetail = "Permission denied"

	// NotFound
	NotFoundType    = "https://kongapi.info/konnect/not-found"
	NotFoundTitle   = "Not Found"
	NotFoundDetail  = "The requested %s was not found"
	NotFoundDetails = "The requested %s were not found: %v"

	// Method Not Allowed
	MethodNotAllowedType   = "https://kongapi.info/konnect/method-not-allowed"
	MethodNotAllowedTitle  = "Method Not Allowed"
	MethodNotAllowedDetail = "The requested method is not allowed"

	// BadRequest
	BadRequestType  = "https://kongapi.info/konnect/bad-request"
	BadRequestTitle = "Bad Request"

	// Gone
	GoneType   = "https://kongapi.info/konnect/gone"
	GoneTitle  = "Gone"
	GoneDetail = "The requested resource is no longer available"

	// Precondition Failed
	PreconditionFailedType  = "https://kongapi.info/konnect/precondition-failed"
	PreconditionFailedTitle = "Precondition Failed"

	// Rate Limit
	RateLimitType   = "https://kongapi.info/konnect/rate-limited"
	RateLimitTitle  = "Rate Limited"
	RateLimitDetail = "Too many requests"

	// Conflict
	ConflictType  = "https://kongapi.info/konnect/resource-conflict"
	ConflictTitle = "Conflict"

	// Empty Set
	EmptySetType       = "Empty Set"
	EmptySetCursorType = "Empty Set Cursor"
)

// NewInternalError generates an internal server error.
func NewInternalError(ctx context.Context, err error) *BaseAPIError {
	return &BaseAPIError{
		Type:            InternalType,
		Status:          http.StatusInternalServerError,
		Title:           InternalTitle,
		Instance:        instance(ctx),
		Detail:          InternalDetail,
		UnderlyingError: err,
		ctx:             ctx,
	}
}

// NewServiceUnavailable generates a not found error.
func NewServiceUnavailable(ctx context.Context, err error) *BaseAPIError {
	return &BaseAPIError{
		Type:            UnavailableType,
		Status:          http.StatusServiceUnavailable,
		Title:           UnavailableTitle,
		Instance:        instance(ctx),
		Detail:          UnavailableDetail,
		UnderlyingError: err,
		ctx:             ctx,
	}
}

// NewUnauthenticatedError generates an unauthenticated error.
func NewUnauthenticatedError(ctx context.Context, err error) *BaseAPIError {
	return &BaseAPIError{
		Type:            UnauthenticatedType,
		Status:          http.StatusUnauthorized,
		Title:           UnauthenticatedTitle,
		Instance:        instance(ctx),
		Detail:          UnauthenticatedDetail,
		UnderlyingError: err,
		ctx:             ctx,
	}
}

// NewForbiddenError generates an unauthorized error.
func NewForbiddenError(ctx context.Context, err error) *BaseAPIError {
	return &BaseAPIError{
		Type:            ForbiddenType,
		Status:          http.StatusForbidden,
		Title:           ForbiddenTitle,
		Instance:        instance(ctx),
		Detail:          ForbiddenDetail,
		UnderlyingError: err,
		ctx:             ctx,
	}
}

// NewForbiddenErrorDetail generates an forbidden error with a user readable/detailed message.
func NewForbiddenErrorDetail(ctx context.Context, detailMessage string) *BaseAPIError {
	return &BaseAPIError{
		Type:     ForbiddenType,
		Status:   http.StatusForbidden,
		Title:    ForbiddenTitle,
		Instance: instance(ctx),
		Detail:   MakeSentenceCase(detailMessage),
		ctx:      ctx,
	}
}

// NewNotFoundError generates a not found error.
func NewNotFoundError(ctx context.Context, err error, entityType string) *BaseAPIError {
	if entityType != "" {
		return &BaseAPIError{
			Type:            NotFoundType,
			Status:          http.StatusNotFound,
			Title:           NotFoundTitle,
			Instance:        instance(ctx),
			Detail:          fmt.Sprintf(NotFoundDetail, entityType),
			UnderlyingError: err,
			ctx:             ctx,
		}
	}
	return &BaseAPIError{
		Type:            NotFoundType,
		Status:          http.StatusNotFound,
		Title:           NotFoundTitle,
		Instance:        instance(ctx),
		UnderlyingError: err,
		ctx:             ctx,
	}
}

// NewNotFoundErrors generates a not found error for multiple resources.
func NewNotFoundErrors(
	ctx context.Context,
	err error,
	entityType string,
	resources any,
) *BaseAPIError {
	return &BaseAPIError{
		Type:            NotFoundType,
		Status:          http.StatusNotFound,
		Title:           NotFoundTitle,
		Instance:        instance(ctx),
		Detail:          fmt.Sprintf(NotFoundDetails, entityType, resources),
		UnderlyingError: err,
		ctx:             ctx,
	}
}

// NewMethodNotAllowedError generates a method not allowed error.
func NewMethodNotAllowedError(ctx context.Context) *BaseAPIError {
	return &BaseAPIError{
		Type:     MethodNotAllowedType,
		Status:   http.StatusMethodNotAllowed,
		Title:    MethodNotAllowedTitle,
		Instance: instance(ctx),
		Detail:   MethodNotAllowedDetail,
		ctx:      ctx,
	}
}

// NewBadRequestError generates a bad request error.
func NewBadRequestError(ctx context.Context, err error, invalidFields InvalidParameters) *BaseAPIError {
	detail := BadRequestTitle
	if len(invalidFields) > 0 {
		detail = fmt.Sprintf("%s: %s", BadRequestTitle, invalidFields.String())
	}
	return &BaseAPIError{
		Type:              BadRequestType,
		Status:            http.StatusBadRequest,
		Title:             BadRequestTitle,
		Instance:          instance(ctx),
		InvalidParameters: invalidFields,
		UnderlyingError:   err,
		Detail:            detail,
		ctx:               ctx,
	}
}

// NewUnsupportedSortFieldError generates a 400 for a `sort` query value naming a
// field the endpoint does not support. v3 list endpoints accept only their
// declared snake_case sort fields; the reason lists the supported ones. Returned
// directly by a resource's sort-field converter so handlers can propagate it
// without re-wrapping.
func NewUnsupportedSortFieldError(ctx context.Context, field string, supported ...string) *BaseAPIError {
	err := fmt.Errorf("unsupported sort field: %s", field)
	return NewBadRequestError(ctx, err, InvalidParameters{
		{
			Field: "sort",
			Reason: fmt.Sprintf(
				"unsupported sort field %q, supported fields: %s",
				field, strings.Join(supported, ", "),
			),
			Source: InvalidParamSourceQuery,
		},
	})
}

// NewGoneError generates a gone error.
func NewGoneError(ctx context.Context, err error) *BaseAPIError {
	return &BaseAPIError{
		Type:            GoneType,
		Status:          http.StatusGone,
		Title:           GoneTitle,
		Instance:        instance(ctx),
		Detail:          GoneDetail,
		UnderlyingError: err,
		ctx:             ctx,
	}
}

// NewPreconditionFailedError generates an precondition failed error.
func NewPreconditionFailedError(ctx context.Context, precondition string) *BaseAPIError {
	return &BaseAPIError{
		Type:            PreconditionFailedType,
		Status:          http.StatusPreconditionFailed,
		Title:           PreconditionFailedTitle,
		Instance:        instance(ctx),
		Detail:          MakeSentenceCase(precondition),
		UnderlyingError: fmt.Errorf("precondition failed: %s", precondition),
		ctx:             ctx,
	}
}

// NewRateLimitError generates an HTTP 429 Too Many Requests error.
func NewRateLimitError(ctx context.Context) *BaseAPIError {
	return &BaseAPIError{
		Type:     RateLimitType,
		Status:   http.StatusTooManyRequests,
		Title:    RateLimitTitle,
		Instance: instance(ctx),
		Detail:   RateLimitDetail,
		ctx:      ctx,
	}
}

func NewConflictError(ctx context.Context, err error, detail string) *BaseAPIError {
	return &BaseAPIError{
		Type:            ConflictType,
		Status:          http.StatusConflict,
		Title:           ConflictTitle,
		Instance:        instance(ctx),
		Detail:          detail,
		UnderlyingError: err,
		ctx:             ctx,
	}
}

func NewEmptySetResponse(ctx context.Context, cursorPagination bool) *BaseAPIError {
	bae := &BaseAPIError{
		Status: http.StatusOK,
		ctx:    ctx,
	}

	if cursorPagination {
		bae.Type = EmptySetCursorType
	} else {
		bae.Type = EmptySetType
	}

	return bae
}

func NewNotImplementedError(ctx context.Context, err error) *BaseAPIError {
	return &BaseAPIError{
		Type:            NotImplementedType,
		Status:          http.StatusNotImplemented,
		Title:           NotImplementedTitle,
		Instance:        instance(ctx),
		Detail:          NotImplementedDetail,
		UnderlyingError: err,
		ctx:             ctx,
	}
}

// MakeSentenceCase takes any string and returns a Sentence case version of it
func MakeSentenceCase(msg string) string {
	if msg == "" {
		return ""
	}
	return strings.ToUpper(msg[:1]) + msg[1:]
}

// instance returns the request ID from the context
// TODO: use trace ID from context instead
func instance(ctx context.Context) string {
	reqID := middleware.GetReqID(ctx)
	if reqID != "" {
		return fmt.Sprintf("urn:request:%s", reqID)
	}
	return ""
}