File size: 6,240 Bytes
04f1444 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 | 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
}
}
|