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 "" }