mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #38536 - Moved PostJSONWithTimeout to platform/http - Created platform/errors package with only types needed by ctxerr. This way, ctxerr did not need to import fleethttp. - Made activity bounded context use PostJSONWithTimeout directly - Removed some activity types from legacy code that were no longer needed # Checklist for submitter - [ ] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. - Changes file `38536-new-activity-bc` already present, and this is just cleanup from that work. ## Testing - [x] Added/updated automated tests - [x] QA'd all new/changed functionality manually <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Release Notes * **Refactor** * Reorganized error handling utilities for improved clarity and decoupling. * Consolidated HTTP utilities to centralize JSON posting functionality with timeout support. * Simplified activity service initialization by removing unused internal parameters. * Cleaned up test utilities and removed webhook-related test scaffolding. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
417 lines
12 KiB
Go
417 lines
12 KiB
Go
package http
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"reflect"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/docker/go-units"
|
|
platform_errors "github.com/fleetdm/fleet/v4/server/platform/errors"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// ErrWithInternal defines an interface for errors that have an internal message
|
|
// that should only be logged, not returned to the client.
|
|
type ErrWithInternal interface {
|
|
error
|
|
// Internal returns the error string that must only be logged internally,
|
|
// not returned to the client.
|
|
Internal() string
|
|
}
|
|
|
|
// ErrWithLogFields defines an interface for errors that have additional log fields.
|
|
type ErrWithLogFields interface {
|
|
error
|
|
// LogFields returns the additional log fields to add, which should come in
|
|
// key, value pairs (as used in go-kit log).
|
|
LogFields() []any
|
|
}
|
|
|
|
// ErrorUUIDer defines an interface for errors that have a UUID for tracking.
|
|
type ErrorUUIDer interface {
|
|
// UUID returns the error's UUID.
|
|
UUID() string
|
|
}
|
|
|
|
// ErrorWithUUID can be embedded in error types to implement ErrorUUIDer.
|
|
type ErrorWithUUID struct {
|
|
uuid string
|
|
}
|
|
|
|
var _ ErrorUUIDer = (*ErrorWithUUID)(nil)
|
|
|
|
// UUID implements the ErrorUUIDer interface.
|
|
func (e *ErrorWithUUID) UUID() string {
|
|
if e.uuid == "" {
|
|
u, err := uuid.NewRandom()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
e.uuid = u.String()
|
|
}
|
|
return e.uuid
|
|
}
|
|
|
|
// BadRequestError is the error returned when the request is invalid.
|
|
type BadRequestError struct {
|
|
Message string
|
|
InternalErr error
|
|
|
|
ErrorWithUUID
|
|
}
|
|
|
|
// Error returns the error message.
|
|
func (e *BadRequestError) Error() string {
|
|
return e.Message
|
|
}
|
|
|
|
// BadRequestError implements the interface required by the server/service package logic
|
|
// to determine the status code to return to the client.
|
|
func (e *BadRequestError) BadRequestError() []map[string]string {
|
|
return nil
|
|
}
|
|
|
|
// Internal implements the ErrWithInternal interface.
|
|
func (e BadRequestError) Internal() string {
|
|
if e.InternalErr != nil {
|
|
return e.InternalErr.Error()
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// We implement the second type of Unwrap that returns an error array, which still works for errors.Is/As, but is not supported in errors.Unwrap
|
|
// This allows us to check the error chain, but not log the most inner error in the HTTP response.
|
|
func (e BadRequestError) Unwrap() []error {
|
|
return []error{e.InternalErr}
|
|
}
|
|
|
|
// IsClientError implements ErrWithIsClientError.
|
|
func (e BadRequestError) IsClientError() bool {
|
|
return true
|
|
}
|
|
|
|
type PayloadTooLargeError struct {
|
|
ContentLength string
|
|
MaxRequestSize int64
|
|
}
|
|
|
|
func (e PayloadTooLargeError) Error() string {
|
|
return fmt.Sprintf("Request exceeds the max size limit of %s. Configure the limit: https://fleetdm.com/docs/configuration/fleet-server-configuration#server-default-max-request-body-size", units.HumanSize(float64(e.MaxRequestSize)))
|
|
}
|
|
|
|
func (e PayloadTooLargeError) Internal() string {
|
|
// This is for us to have an indication of the size we get, might be spoofable, but better than nothing
|
|
msg := fmt.Sprintf("Request exceeds the max size limit of %s", units.HumanSize(float64(e.MaxRequestSize)))
|
|
if e.ContentLength != "" {
|
|
size := e.ContentLength
|
|
contentLengthAsNumber, err := strconv.ParseFloat(e.ContentLength, 64)
|
|
if err == nil {
|
|
// We don't care if we failed to parse the number, only if we were successful
|
|
size = units.HumanSize(contentLengthAsNumber)
|
|
}
|
|
msg += fmt.Sprintf(", Incoming Content-Length: %s", size)
|
|
}
|
|
return msg
|
|
}
|
|
|
|
func (e PayloadTooLargeError) StatusCode() int {
|
|
return http.StatusRequestEntityTooLarge
|
|
}
|
|
|
|
func (e PayloadTooLargeError) IsClientError() bool {
|
|
return true
|
|
}
|
|
|
|
// UserMessageError is an error that wraps another error with a user-friendly message.
|
|
type UserMessageError struct {
|
|
error
|
|
statusCode int
|
|
|
|
ErrorWithUUID
|
|
}
|
|
|
|
// NewUserMessageError creates a UserMessageError that will translate the
|
|
// error message of err to a user-friendly form. If statusCode is > 0, it
|
|
// will be used as the HTTP status code for the error, otherwise it defaults
|
|
// to http.StatusUnprocessableEntity (422).
|
|
func NewUserMessageError(err error, statusCode int) *UserMessageError {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
return &UserMessageError{
|
|
error: err,
|
|
statusCode: statusCode,
|
|
}
|
|
}
|
|
|
|
// StatusCode returns the HTTP status code for this error.
|
|
func (e UserMessageError) StatusCode() int {
|
|
if e.statusCode > 0 {
|
|
return e.statusCode
|
|
}
|
|
return http.StatusUnprocessableEntity
|
|
}
|
|
|
|
// IsClientError implements ErrWithIsClientError.
|
|
// Returns true for 4xx status codes, false for 5xx.
|
|
func (e UserMessageError) IsClientError() bool {
|
|
code := e.StatusCode()
|
|
return code >= 400 && code < 500
|
|
}
|
|
|
|
var rxJSONUnknownField = regexp.MustCompile(`^json: unknown field "(.+)"$`)
|
|
|
|
// IsJSONUnknownFieldError returns true if err is a JSON unknown field error.
|
|
// There is no exported type or value for this error, so we have to match the
|
|
// error message.
|
|
func IsJSONUnknownFieldError(err error) bool {
|
|
return rxJSONUnknownField.MatchString(err.Error())
|
|
}
|
|
|
|
// GetJSONUnknownField returns the unknown field name from a JSON unknown field error.
|
|
func GetJSONUnknownField(err error) *string {
|
|
errCause := platform_errors.Cause(err)
|
|
if IsJSONUnknownFieldError(errCause) {
|
|
substr := rxJSONUnknownField.FindStringSubmatch(errCause.Error())
|
|
return &substr[1]
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// UserMessage implements the user-friendly translation of the error if its
|
|
// root cause is one of the supported types, otherwise it returns the error
|
|
// message.
|
|
func (e UserMessageError) UserMessage() string {
|
|
cause := platform_errors.Cause(e.error)
|
|
switch cause := cause.(type) {
|
|
case *json.UnmarshalTypeError:
|
|
var sb strings.Builder
|
|
curType := cause.Type
|
|
for curType.Kind() == reflect.Slice || curType.Kind() == reflect.Array {
|
|
sb.WriteString("array of ")
|
|
curType = curType.Elem()
|
|
}
|
|
sb.WriteString(curType.Name())
|
|
if curType != cause.Type {
|
|
// it was an array
|
|
sb.WriteString("s")
|
|
}
|
|
|
|
return fmt.Sprintf("invalid value type at '%s': expected %s but got %s", cause.Field, sb.String(), cause.Value)
|
|
|
|
default:
|
|
// there's no specific error type for the strict json mode
|
|
// (DisallowUnknownFields), so resort to message-matching.
|
|
if matches := rxJSONUnknownField.FindStringSubmatch(cause.Error()); matches != nil {
|
|
return fmt.Sprintf("unsupported key provided: %q", matches[1])
|
|
}
|
|
return e.Error()
|
|
}
|
|
}
|
|
|
|
// ErrWithRetryAfter is an interface for errors that should set a specific HTTP
|
|
// Header Retry-After value (see
|
|
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After)
|
|
type ErrWithRetryAfter interface {
|
|
error
|
|
// RetryAfter returns the number of seconds to wait before retry.
|
|
RetryAfter() int
|
|
}
|
|
|
|
// ForeignKeyError is an interface for errors caused by foreign key constraint violations.
|
|
type ForeignKeyError interface {
|
|
error
|
|
IsForeignKey() bool
|
|
}
|
|
|
|
// IsForeignKey returns true if err is a foreign key constraint violation.
|
|
func IsForeignKey(err error) bool {
|
|
var fke ForeignKeyError
|
|
if errors.As(err, &fke) {
|
|
return fke.IsForeignKey()
|
|
}
|
|
return false
|
|
}
|
|
|
|
// AlreadyExistsError is an interface for errors when a resource already exists.
|
|
type AlreadyExistsError interface {
|
|
error
|
|
IsExists() bool
|
|
}
|
|
|
|
// Error is a generic error type with a code and message.
|
|
type Error struct {
|
|
Code int `json:"code,omitempty"`
|
|
Message string `json:"message,omitempty"`
|
|
|
|
ErrorWithUUID
|
|
}
|
|
|
|
// Error returns the error message.
|
|
func (e *Error) Error() string {
|
|
return e.Message
|
|
}
|
|
|
|
// AuthFailedError is returned when authentication fails.
|
|
type AuthFailedError struct {
|
|
// internal is the reason that should only be logged internally
|
|
internal string
|
|
|
|
ErrorWithUUID
|
|
}
|
|
|
|
// NewAuthFailedError creates a new AuthFailedError.
|
|
func NewAuthFailedError(internal string) *AuthFailedError {
|
|
return &AuthFailedError{internal: internal}
|
|
}
|
|
|
|
// Error implements the error interface.
|
|
func (e AuthFailedError) Error() string {
|
|
return "Authentication failed"
|
|
}
|
|
|
|
// Internal implements ErrWithInternal.
|
|
func (e AuthFailedError) Internal() string {
|
|
return e.internal
|
|
}
|
|
|
|
// StatusCode implements kithttp.StatusCoder.
|
|
func (e AuthFailedError) StatusCode() int {
|
|
return http.StatusUnauthorized
|
|
}
|
|
|
|
// IsClientError implements ErrWithIsClientError.
|
|
func (e AuthFailedError) IsClientError() bool {
|
|
return true
|
|
}
|
|
|
|
// AuthRequiredError is returned when authentication is required.
|
|
type AuthRequiredError struct {
|
|
// internal is the reason that should only be logged internally
|
|
internal string
|
|
|
|
ErrorWithUUID
|
|
}
|
|
|
|
// NewAuthRequiredError creates a new AuthRequiredError.
|
|
func NewAuthRequiredError(internal string) *AuthRequiredError {
|
|
return &AuthRequiredError{internal: internal}
|
|
}
|
|
|
|
// Error implements the error interface.
|
|
func (e AuthRequiredError) Error() string {
|
|
return "Authentication required"
|
|
}
|
|
|
|
// Internal implements ErrWithInternal.
|
|
func (e AuthRequiredError) Internal() string {
|
|
return e.internal
|
|
}
|
|
|
|
// StatusCode implements kithttp.StatusCoder.
|
|
func (e AuthRequiredError) StatusCode() int {
|
|
return http.StatusUnauthorized
|
|
}
|
|
|
|
// IsClientError implements ErrWithIsClientError.
|
|
func (e AuthRequiredError) IsClientError() bool {
|
|
return true
|
|
}
|
|
|
|
// AuthHeaderRequiredError is returned when an authorization header is required.
|
|
type AuthHeaderRequiredError struct {
|
|
// internal is the reason that should only be logged internally
|
|
internal string
|
|
|
|
ErrorWithUUID
|
|
}
|
|
|
|
// NewAuthHeaderRequiredError creates a new AuthHeaderRequiredError.
|
|
func NewAuthHeaderRequiredError(internal string) *AuthHeaderRequiredError {
|
|
return &AuthHeaderRequiredError{
|
|
internal: internal,
|
|
}
|
|
}
|
|
|
|
// Error implements the error interface.
|
|
func (e AuthHeaderRequiredError) Error() string {
|
|
return "Authorization header required"
|
|
}
|
|
|
|
// Internal implements ErrWithInternal.
|
|
func (e AuthHeaderRequiredError) Internal() string {
|
|
return e.internal
|
|
}
|
|
|
|
// StatusCode implements kithttp.StatusCoder.
|
|
func (e AuthHeaderRequiredError) StatusCode() int {
|
|
return http.StatusUnauthorized
|
|
}
|
|
|
|
// IsClientError implements ErrWithIsClientError.
|
|
func (e AuthHeaderRequiredError) IsClientError() bool {
|
|
return true
|
|
}
|
|
|
|
// ErrPasswordResetRequired is returned when a password reset is required.
|
|
var ErrPasswordResetRequired = &passwordResetRequiredError{}
|
|
|
|
type passwordResetRequiredError struct {
|
|
ErrorWithUUID
|
|
}
|
|
|
|
// Error implements the error interface.
|
|
func (e passwordResetRequiredError) Error() string {
|
|
return "password reset required"
|
|
}
|
|
|
|
// StatusCode implements kithttp.StatusCoder.
|
|
func (e passwordResetRequiredError) StatusCode() int {
|
|
return http.StatusUnauthorized
|
|
}
|
|
|
|
// IsClientError implements ErrWithIsClientError.
|
|
func (e passwordResetRequiredError) IsClientError() bool {
|
|
return true
|
|
}
|
|
|
|
// ForbiddenErrorMessage is the error message that should be returned to
|
|
// clients when an action is forbidden. It is intentionally vague to prevent
|
|
// disclosing information that a client should not have access to.
|
|
const ForbiddenErrorMessage = "forbidden"
|
|
|
|
// CheckMissing is the error to return when no authorization check was performed
|
|
// by the service.
|
|
type CheckMissing struct {
|
|
response any
|
|
|
|
ErrorWithUUID
|
|
}
|
|
|
|
// CheckMissingWithResponse creates a new error indicating the authorization
|
|
// check was missed, and including the response for further analysis by the error
|
|
// encoder.
|
|
func CheckMissingWithResponse(response any) *CheckMissing {
|
|
return &CheckMissing{response: response}
|
|
}
|
|
|
|
// Error implements the error interface.
|
|
func (e *CheckMissing) Error() string {
|
|
return ForbiddenErrorMessage
|
|
}
|
|
|
|
// Internal implements the ErrWithInternal interface.
|
|
func (e *CheckMissing) Internal() string {
|
|
return "Missing authorization check"
|
|
}
|
|
|
|
// Response returns the response that was generated before the authorization
|
|
// check was found to be missing.
|
|
func (e *CheckMissing) Response() any {
|
|
return e.response
|
|
}
|