| package apierrors |
|
|
| import ( |
| "context" |
| "errors" |
| "net/http" |
| "strings" |
|
|
| api "github.com/openmeterio/openmeter/api/v3" |
| "github.com/openmeterio/openmeter/pkg/errorsx" |
| "github.com/openmeterio/openmeter/pkg/models" |
| ) |
|
|
| const httpStatusCodeErrorAttribute = "openmeter.http.status_code" |
|
|
| |
| |
| |
| |
| |
| func NewV3ErrorHandlerFunc(logger errorsx.Handler) func(w http.ResponseWriter, r *http.Request, err error) { |
| return func(w http.ResponseWriter, r *http.Request, err error) { |
| if err == nil { |
| return |
| } |
|
|
| |
| var apiErr *BaseAPIError |
| if errors.As(err, &apiErr) { |
| apiErr.HandleAPIError(w, r) |
| return |
| } |
|
|
| ctx := r.Context() |
|
|
| |
| |
| if invalidParams, ok := invalidParametersFromGeneratedRouterError(err); ok { |
| logger.HandleContext(ctx, err) |
| NewBadRequestError(ctx, err, invalidParams).HandleAPIError(w, r) |
| return |
| } |
|
|
| |
| if status, ok := singularHTTPStatusFromValidationIssues(err); ok { |
| if mapped := apiErrorFromHTTPStatus(ctx, status, err); mapped != nil { |
| logger.HandleContext(ctx, err) |
| mapped.HandleAPIError(w, r) |
| return |
| } |
| } |
|
|
| |
| validationErr := models.NewGenericValidationError(err) |
| logger.HandleContext(r.Context(), validationErr) |
| NewBadRequestError(r.Context(), validationErr, nil).HandleAPIError(w, r) |
| } |
| } |
|
|
| func invalidParametersFromGeneratedRouterError(err error) (InvalidParameters, bool) { |
| |
| |
| |
| |
| |
| var invalidFormat *api.InvalidParamFormatError |
| if errors.As(err, &invalidFormat) { |
| field := enrichFieldFromBindError(invalidFormat.ParamName, invalidFormat.Err.Error()) |
| return InvalidParameters{ |
| { |
| Field: field, |
| Rule: "format", |
| Reason: invalidFormat.Err.Error(), |
| Source: InvalidParamSourceQuery, |
| }, |
| }, true |
| } |
|
|
| var requiredParam *api.RequiredParamError |
| if errors.As(err, &requiredParam) { |
| return InvalidParameters{ |
| { |
| Field: requiredParam.ParamName, |
| Rule: "required", |
| Reason: "is required", |
| Source: InvalidParamSourceQuery, |
| }, |
| }, true |
| } |
|
|
| var requiredHeader *api.RequiredHeaderError |
| if errors.As(err, &requiredHeader) { |
| return InvalidParameters{ |
| { |
| Field: requiredHeader.ParamName, |
| Rule: "required", |
| Reason: "is required", |
| Source: InvalidParamSourceHeader, |
| }, |
| }, true |
| } |
|
|
| var tooMany *api.TooManyValuesForParamError |
| if errors.As(err, &tooMany) { |
| return InvalidParameters{ |
| { |
| Field: tooMany.ParamName, |
| Rule: "too_many_values", |
| Reason: tooMany.Error(), |
| Source: InvalidParamSourceQuery, |
| }, |
| }, true |
| } |
|
|
| var unmarshal *api.UnmarshalingParamError |
| if errors.As(err, &unmarshal) { |
| return InvalidParameters{ |
| { |
| Field: unmarshal.ParamName, |
| Rule: "unmarshal", |
| Reason: unmarshal.Err.Error(), |
| Source: InvalidParamSourceQuery, |
| }, |
| }, true |
| } |
|
|
| var unescapedCookie *api.UnescapedCookieParamError |
| if errors.As(err, &unescapedCookie) { |
| return InvalidParameters{ |
| { |
| Field: unescapedCookie.ParamName, |
| Rule: "unescape", |
| Reason: unescapedCookie.Error(), |
| Source: InvalidParamSourceHeader, |
| }, |
| }, true |
| } |
|
|
| return nil, false |
| } |
|
|
| func enrichFieldFromBindError(paramName string, bindErrMsg string) string { |
| |
| |
| |
| |
| |
| if paramName == "" || bindErrMsg == "" { |
| return paramName |
| } |
| if strings.Contains(paramName, "[") { |
| |
| return paramName |
| } |
| const needle = "field [" |
| i := strings.Index(bindErrMsg, needle) |
| if i == -1 { |
| return paramName |
| } |
| rest := bindErrMsg[i+len(needle):] |
| j := strings.Index(rest, "]") |
| if j == -1 { |
| return paramName |
| } |
| field := rest[:j] |
| if field == "" { |
| return paramName |
| } |
| return paramName + "." + field |
| } |
|
|
| func singularHTTPStatusFromValidationIssues(err error) (int, bool) { |
| issues, _ := models.AsValidationIssues(err) |
| if len(issues) == 0 { |
| return 0, false |
| } |
|
|
| |
| |
| codes := make(map[int]struct{}, 1) |
| for _, issue := range issues { |
| raw, ok := issue.Attributes()[httpStatusCodeErrorAttribute] |
| if !ok { |
| continue |
| } |
| c, ok := raw.(int) |
| if !ok { |
| continue |
| } |
| codes[c] = struct{}{} |
| } |
|
|
| if len(codes) != 1 { |
| return 0, false |
| } |
|
|
| for c := range codes { |
| return c, true |
| } |
| return 0, false |
| } |
|
|
| func apiErrorFromHTTPStatus(ctx context.Context, status int, err error) *BaseAPIError { |
| switch status { |
| case http.StatusBadRequest: |
| return NewBadRequestError(ctx, err, nil) |
| case http.StatusUnauthorized: |
| return NewUnauthenticatedError(ctx, err) |
| case http.StatusForbidden: |
| return NewForbiddenError(ctx, err) |
| case http.StatusNotFound: |
| return NewNotFoundError(ctx, err, "") |
| case http.StatusConflict: |
| return NewConflictError(ctx, err, err.Error()) |
| case http.StatusPreconditionFailed: |
| return NewPreconditionFailedError(ctx, err.Error()) |
| case http.StatusNotImplemented: |
| return NewNotImplementedError(ctx, err) |
| default: |
| return nil |
| } |
| } |
|
|