Download api/v3/apierrors/handler.go from Leon4gr45/openmeter: direct link, hf CLI and curl.
- Browser
- Download file 6.24 kB
-
https://huggingface.co/spaces/Leon4gr45/openmeter/resolve/main/api/v3/apierrors/handler.go
- Command line
-
hf download hf://spaces/Leon4gr45/openmeter/api/v3/apierrors/handler.go
-
curl -L -o handler.go https://huggingface.co/spaces/Leon4gr45/openmeter/resolve/main/api/v3/apierrors/handler.go
6.24 kB
| 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" | |
| // NewV3ErrorHandlerFunc returns an oapi-codegen ChiServerOptions.ErrorHandlerFunc implementation. | |
| // | |
| // It is invoked when the generated router fails request binding (query/path/header parsing). | |
| // The main purpose is to ensure we always write a response (otherwise net/http defaults to 200 with | |
| // an empty body), and to keep error-to-status mapping consistent with our model error types. | |
| 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 | |
| } | |
| // If it's already a v3 API error, just render it. | |
| var apiErr *BaseAPIError | |
| if errors.As(err, &apiErr) { | |
| apiErr.HandleAPIError(w, r) | |
| return | |
| } | |
| ctx := r.Context() | |
| // Request binding errors produced by the generated v3 router. | |
| // Convert them into v3 InvalidParameters so the response is actionable for clients. | |
| if invalidParams, ok := invalidParametersFromGeneratedRouterError(err); ok { | |
| logger.HandleContext(ctx, err) | |
| NewBadRequestError(ctx, err, invalidParams).HandleAPIError(w, r) | |
| return | |
| } | |
| // Mirror commonhttp.GenericErrorEncoder's ordering, but render using v3 apierrors. | |
| if status, ok := singularHTTPStatusFromValidationIssues(err); ok { | |
| if mapped := apiErrorFromHTTPStatus(ctx, status, err); mapped != nil { | |
| logger.HandleContext(ctx, err) | |
| mapped.HandleAPIError(w, r) | |
| return | |
| } | |
| } | |
| // Default: classify as validation error (400) for request binding failures. | |
| validationErr := models.NewGenericValidationError(err) | |
| logger.HandleContext(r.Context(), validationErr) | |
| NewBadRequestError(r.Context(), validationErr, nil).HandleAPIError(w, r) | |
| } | |
| } | |
| func invalidParametersFromGeneratedRouterError(err error) (InvalidParameters, bool) { | |
| // These types are defined in api/v3/api.gen.go. | |
| // | |
| // Note: those errors do not carry the parameter location (query/path/header) except for the | |
| // dedicated "required header" variant, so we default to "query" where ambiguous. This is still | |
| // a major improvement over returning an empty error response. | |
| 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 { | |
| // oapi-codegen deepObject binding errors (runtime.BindQueryParameter) can be more specific than | |
| // just the outer parameter name, e.g.: | |
| // "error assigning value to destination: field [sizee] is not present in destination object". | |
| // | |
| // For nicer AIP errors, return "page.sizee" instead of just "page". | |
| if paramName == "" || bindErrMsg == "" { | |
| return paramName | |
| } | |
| if strings.Contains(paramName, "[") { | |
| // Already specific (e.g. "page[size]") - keep as-is. | |
| 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 | |
| } | |
| // We intentionally mirror commonhttp.HandleIssueIfHTTPStatusKnown's "singular" behavior: | |
| // if multiple status codes are present, we don't map. | |
| 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 | |
| } | |
| } | |