mirror of
https://github.com/pocketbase/pocketbase.git
synced 2025-03-20 06:21:06 +02:00
232 lines
6.0 KiB
Go
232 lines
6.0 KiB
Go
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
|
|
}
|