Adding upstream version 0.28.1.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
88f1d47ab6
commit
e28c88ef14
933 changed files with 194711 additions and 0 deletions
231
tools/router/error.go
Normal file
231
tools/router/error.go
Normal file
|
@ -0,0 +1,231 @@
|
|||
package router
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
validation "github.com/go-ozzo/ozzo-validation/v4"
|
||||
"github.com/pocketbase/pocketbase/tools/inflector"
|
||||
)
|
||||
|
||||
// SafeErrorItem defines a common error interface for a printable public safe error.
|
||||
type SafeErrorItem interface {
|
||||
// Code represents a fixed unique identifier of the error (usually used as translation key).
|
||||
Code() string
|
||||
|
||||
// Error is the default English human readable error message that will be returned.
|
||||
Error() string
|
||||
}
|
||||
|
||||
// SafeErrorParamsResolver defines an optional interface for specifying dynamic error parameters.
|
||||
type SafeErrorParamsResolver interface {
|
||||
// Params defines a map with dynamic parameters to return as part of the public safe error view.
|
||||
Params() map[string]any
|
||||
}
|
||||
|
||||
// SafeErrorResolver defines an error interface for resolving the public safe error fields.
|
||||
type SafeErrorResolver interface {
|
||||
// Resolve allows modifying and returning a new public safe error data map.
|
||||
Resolve(errData map[string]any) any
|
||||
}
|
||||
|
||||
// ApiError defines the struct for a basic api error response.
|
||||
type ApiError struct {
|
||||
rawData any
|
||||
|
||||
Data map[string]any `json:"data"`
|
||||
Message string `json:"message"`
|
||||
Status int `json:"status"`
|
||||
}
|
||||
|
||||
// Error makes it compatible with the `error` interface.
|
||||
func (e *ApiError) Error() string {
|
||||
return e.Message
|
||||
}
|
||||
|
||||
// RawData returns the unformatted error data (could be an internal error, text, etc.)
|
||||
func (e *ApiError) RawData() any {
|
||||
return e.rawData
|
||||
}
|
||||
|
||||
// Is reports whether the current ApiError wraps the target.
|
||||
func (e *ApiError) Is(target error) bool {
|
||||
err, ok := e.rawData.(error)
|
||||
if ok {
|
||||
return errors.Is(err, target)
|
||||
}
|
||||
|
||||
apiErr, ok := target.(*ApiError)
|
||||
|
||||
return ok && e == apiErr
|
||||
}
|
||||
|
||||
// NewNotFoundError creates and returns 404 ApiError.
|
||||
func NewNotFoundError(message string, rawErrData any) *ApiError {
|
||||
if message == "" {
|
||||
message = "The requested resource wasn't found."
|
||||
}
|
||||
|
||||
return NewApiError(http.StatusNotFound, message, rawErrData)
|
||||
}
|
||||
|
||||
// NewBadRequestError creates and returns 400 ApiError.
|
||||
func NewBadRequestError(message string, rawErrData any) *ApiError {
|
||||
if message == "" {
|
||||
message = "Something went wrong while processing your request."
|
||||
}
|
||||
|
||||
return NewApiError(http.StatusBadRequest, message, rawErrData)
|
||||
}
|
||||
|
||||
// NewForbiddenError creates and returns 403 ApiError.
|
||||
func NewForbiddenError(message string, rawErrData any) *ApiError {
|
||||
if message == "" {
|
||||
message = "You are not allowed to perform this request."
|
||||
}
|
||||
|
||||
return NewApiError(http.StatusForbidden, message, rawErrData)
|
||||
}
|
||||
|
||||
// NewUnauthorizedError creates and returns 401 ApiError.
|
||||
func NewUnauthorizedError(message string, rawErrData any) *ApiError {
|
||||
if message == "" {
|
||||
message = "Missing or invalid authentication."
|
||||
}
|
||||
|
||||
return NewApiError(http.StatusUnauthorized, message, rawErrData)
|
||||
}
|
||||
|
||||
// NewInternalServerError creates and returns 500 ApiError.
|
||||
func NewInternalServerError(message string, rawErrData any) *ApiError {
|
||||
if message == "" {
|
||||
message = "Something went wrong while processing your request."
|
||||
}
|
||||
|
||||
return NewApiError(http.StatusInternalServerError, message, rawErrData)
|
||||
}
|
||||
|
||||
func NewTooManyRequestsError(message string, rawErrData any) *ApiError {
|
||||
if message == "" {
|
||||
message = "Too Many Requests."
|
||||
}
|
||||
|
||||
return NewApiError(http.StatusTooManyRequests, message, rawErrData)
|
||||
}
|
||||
|
||||
// NewApiError creates and returns new normalized ApiError instance.
|
||||
func NewApiError(status int, message string, rawErrData any) *ApiError {
|
||||
if message == "" {
|
||||
message = http.StatusText(status)
|
||||
}
|
||||
|
||||
return &ApiError{
|
||||
rawData: rawErrData,
|
||||
Data: safeErrorsData(rawErrData),
|
||||
Status: status,
|
||||
Message: strings.TrimSpace(inflector.Sentenize(message)),
|
||||
}
|
||||
}
|
||||
|
||||
// ToApiError wraps err into ApiError instance (if not already).
|
||||
func ToApiError(err error) *ApiError {
|
||||
var apiErr *ApiError
|
||||
|
||||
if !errors.As(err, &apiErr) {
|
||||
// no ApiError found -> assign a generic one
|
||||
if errors.Is(err, sql.ErrNoRows) || errors.Is(err, fs.ErrNotExist) {
|
||||
apiErr = NewNotFoundError("", err)
|
||||
} else {
|
||||
apiErr = NewBadRequestError("", err)
|
||||
}
|
||||
}
|
||||
|
||||
return apiErr
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
func safeErrorsData(data any) map[string]any {
|
||||
switch v := data.(type) {
|
||||
case validation.Errors:
|
||||
return resolveSafeErrorsData(v)
|
||||
case error:
|
||||
validationErrors := validation.Errors{}
|
||||
if errors.As(v, &validationErrors) {
|
||||
return resolveSafeErrorsData(validationErrors)
|
||||
}
|
||||
return map[string]any{} // not nil to ensure that is json serialized as object
|
||||
case map[string]validation.Error:
|
||||
return resolveSafeErrorsData(v)
|
||||
case map[string]SafeErrorItem:
|
||||
return resolveSafeErrorsData(v)
|
||||
case map[string]error:
|
||||
return resolveSafeErrorsData(v)
|
||||
case map[string]string:
|
||||
return resolveSafeErrorsData(v)
|
||||
case map[string]any:
|
||||
return resolveSafeErrorsData(v)
|
||||
default:
|
||||
return map[string]any{} // not nil to ensure that is json serialized as object
|
||||
}
|
||||
}
|
||||
|
||||
func resolveSafeErrorsData[T any](data map[string]T) map[string]any {
|
||||
result := map[string]any{}
|
||||
|
||||
for name, err := range data {
|
||||
if isNestedError(err) {
|
||||
result[name] = safeErrorsData(err)
|
||||
} else {
|
||||
result[name] = resolveSafeErrorItem(err)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func isNestedError(err any) bool {
|
||||
switch err.(type) {
|
||||
case validation.Errors,
|
||||
map[string]validation.Error,
|
||||
map[string]SafeErrorItem,
|
||||
map[string]error,
|
||||
map[string]string,
|
||||
map[string]any:
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// resolveSafeErrorItem extracts from each validation error its
|
||||
// public safe error code and message.
|
||||
func resolveSafeErrorItem(err any) any {
|
||||
data := map[string]any{}
|
||||
|
||||
if obj, ok := err.(SafeErrorItem); ok {
|
||||
// extract the specific error code and message
|
||||
data["code"] = obj.Code()
|
||||
data["message"] = inflector.Sentenize(obj.Error())
|
||||
} else {
|
||||
// fallback to the default public safe values
|
||||
data["code"] = "validation_invalid_value"
|
||||
data["message"] = "Invalid value."
|
||||
}
|
||||
|
||||
if s, ok := err.(SafeErrorParamsResolver); ok {
|
||||
params := s.Params()
|
||||
if len(params) > 0 {
|
||||
data["params"] = params
|
||||
}
|
||||
}
|
||||
|
||||
if s, ok := err.(SafeErrorResolver); ok {
|
||||
return s.Resolve(data)
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue