openmeter / api /v3 /apierrors /handler.go
Leon4gr45's picture
Upload folder using huggingface_hub (part 3)
04f1444 verified
Raw
History Blame Contribute Delete
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
}
}