Leon4gr45's picture
Upload folder using huggingface_hub (part 9)
fea99b3 verified
Raw
History Blame Contribute Delete
6.18 kB
package commonhttp
import (
"context"
"errors"
"log/slog"
"net/http"
"github.com/samber/lo"
"github.com/openmeterio/openmeter/pkg/models"
)
type ErrorWithHTTPStatusCode struct {
error
StatusCode int
Extensions []ExtendProblemFunc
}
func ExtendProblem(name, details string) ExtendProblemFunc {
return func() map[string]interface{} {
return map[string]interface{}{
name: details,
}
}
}
type ExtendProblemFunc func() map[string]interface{}
func (e ExtendProblemFunc) apply(extensions map[string]interface{}) {
for k, v := range e() {
extensions[k] = v
}
}
func NewHTTPError(statusCode int, err error, extensions ...ExtendProblemFunc) ErrorWithHTTPStatusCode {
return ErrorWithHTTPStatusCode{
StatusCode: statusCode,
error: err,
Extensions: extensions,
}
}
func (e ErrorWithHTTPStatusCode) EncodeError(ctx context.Context, w http.ResponseWriter) bool {
problem := models.NewStatusProblem(ctx, e.error, e.StatusCode)
for _, ext := range e.Extensions {
ext.apply(problem.Extensions)
}
problem.Respond(w)
return true
}
// ErrorEncoder encodes an error as HTTP 500 Internal Server Error.
func ErrorEncoder(ctx context.Context, _ error, w http.ResponseWriter) bool {
models.NewStatusProblem(ctx, errors.New("something went wrong"), http.StatusInternalServerError).Respond(w)
return false
}
// HandleErrorIfTypeMatches checks if the error is of the given type and encodes it as an HTTP error.
// Using the generic feature we can mandate that the error implements the error interface. This is a
// must, as the errors.As would panic if the error does not implement the error interface.
func HandleErrorIfTypeMatches[T error](ctx context.Context, statusCode int, err error, w http.ResponseWriter, extendedProblemFunc ...func(T) map[string]interface{}) bool {
if err, ok := lo.ErrorsAs[T](err); ok {
extendedProblemFuncs := make([]ExtendProblemFunc, 0, len(extendedProblemFunc))
for _, f := range extendedProblemFunc {
extendedProblemFuncs = append(extendedProblemFuncs, func() map[string]interface{} {
return f(err)
})
}
NewHTTPError(statusCode, err, extendedProblemFuncs...).EncodeError(ctx, w)
return true
}
return false
}
type httpAttributeKey string
const httpStatusCodeErrorAttribute httpAttributeKey = "openmeter.http.status_code"
func WithHTTPStatusCodeAttribute(code int) models.ValidationIssueOption {
return models.WithAttribute(httpStatusCodeErrorAttribute, code)
}
// Options for the Handler methods
type handleIssueIfHTTPStatusKnownOptions struct {
statusPriorizationBehavior HTTPStatusAttributePriorizationBehavior
}
type HTTPStatusAttributePriorizationBehavior string
const (
// In case there are multiple status codes, the errors won't be mapped. This is the default and recommended behavior
HTTPStatusAttributePriorizationBehaviorSingular HTTPStatusAttributePriorizationBehavior = "singular"
// This below options could be added but I fear they'd only cause more confusion
//
// // In case of conflicts, the status of the first error will be used
// HTTPStatusAttributePriorizationBehaviorFirst HTTPStatusAttributePriorizationBehavior = "first"
// // In case of conflicts, if there's an auth-authz error, it will be used, otherwise the status of the first error will be used
// HTTPStatusAttributePriorizationBehaviorAuthAuthz HTTPStatusAttributePriorizationBehavior = "auth-authz"
)
type HandleIssueIfHTTPStatusKnownOptions func(*handleIssueIfHTTPStatusKnownOptions)
func WithHTTPStatusAttributePriorizationBehavior(behavior HTTPStatusAttributePriorizationBehavior) HandleIssueIfHTTPStatusKnownOptions {
return func(o *handleIssueIfHTTPStatusKnownOptions) {
o.statusPriorizationBehavior = behavior
}
}
func HandleIssueIfHTTPStatusKnown(ctx context.Context, err error, w http.ResponseWriter, options ...HandleIssueIfHTTPStatusKnownOptions) bool {
opts := &handleIssueIfHTTPStatusKnownOptions{
statusPriorizationBehavior: HTTPStatusAttributePriorizationBehaviorSingular,
}
for _, opt := range options {
opt(opts)
}
issues, err := models.AsValidationIssues(err)
if err != nil {
return false
}
if len(issues) == 0 {
return false
}
issuesByCodeMap := make(map[int]models.ValidationIssues)
for _, issue := range issues {
code, ok := issue.Attributes()[httpStatusCodeErrorAttribute]
if !ok {
continue
}
issueCode, ok := code.(int)
if !ok {
slog.Default().DebugContext(ctx, "issue does have HTTP status code attribute but it's not an integer", "issue", issue)
continue
}
issues := issuesByCodeMap[issueCode]
issues = append(issues, issue)
issuesByCodeMap[issueCode] = issues
}
if len(issuesByCodeMap) == 0 {
return false
}
extendProblemFuncs := make([]ExtendProblemFunc, 0)
responseStatusCode := 500 // default to internal server error
switch opts.statusPriorizationBehavior {
case HTTPStatusAttributePriorizationBehaviorSingular:
if len(issuesByCodeMap) > 1 {
return false
}
for code, issues := range issuesByCodeMap {
responseStatusCode = code
extendProblemFuncs = append(extendProblemFuncs, func() map[string]interface{} {
return map[string]interface{}{
// FIXME[galexi,chrisgacsal]: having everything under "validationErrors" makes no sense but we need it for backwards compatibility, otherwise its just hacky...
// should migrate to more generic form like "errors"
"validationErrors": lo.Map(issues, func(issue models.ValidationIssue, _ int) map[string]interface{} {
// We don't want to expose private attributes to the client
attrs := issue.Attributes()
delete(attrs, httpStatusCodeErrorAttribute)
delete(attrs, issue.Code())
issue = issue.SetAttributes(attrs)
return issue.AsErrorExtension()
}),
}
})
}
default:
slog.Default().ErrorContext(ctx, "Unknown HTTP status attribute priorization behavior, passing to next error handler", "behavior", opts.statusPriorizationBehavior)
return false
}
// The returned error message will be a joined error(ValidationIssues.AsError().Error()), not sure if this is the best approach
NewHTTPError(responseStatusCode, err, extendProblemFuncs...).EncodeError(ctx, w)
return true
}