| 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 |
| } |
|
|
| |
| func ErrorEncoder(ctx context.Context, _ error, w http.ResponseWriter) bool { |
| models.NewStatusProblem(ctx, errors.New("something went wrong"), http.StatusInternalServerError).Respond(w) |
|
|
| return false |
| } |
|
|
| |
| |
| |
| 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) |
| } |
|
|
| |
| type handleIssueIfHTTPStatusKnownOptions struct { |
| statusPriorizationBehavior HTTPStatusAttributePriorizationBehavior |
| } |
|
|
| type HTTPStatusAttributePriorizationBehavior string |
|
|
| const ( |
| |
| HTTPStatusAttributePriorizationBehaviorSingular HTTPStatusAttributePriorizationBehavior = "singular" |
|
|
| |
| |
| |
| |
| |
| |
| ) |
|
|
| 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 |
|
|
| 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{}{ |
| |
| |
| "validationErrors": lo.Map(issues, func(issue models.ValidationIssue, _ int) map[string]interface{} { |
| |
| 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 |
| } |
|
|
| |
| NewHTTPError(responseStatusCode, err, extendProblemFuncs...).EncodeError(ctx, w) |
|
|
| return true |
| } |
|
|