Skip to content

Commit 1edf0b2

Browse files
committed
feat: improve error reporting and add structured error codes
Extend error responses with additional JSON fields and HTTP headers to provide richer diagnostic information. Introduce new error codes (token_expired, quota_exhausted) to support programmatic handling and automatic token refresh. Add a helper for extracting the extended metadata into the Error struct. Includes general refactoring to improve code consistency across the codebase.
1 parent a0018c9 commit 1edf0b2

File tree

11 files changed

+601
-99
lines changed

11 files changed

+601
-99
lines changed

pkg/messaging/consumer/cascadingdelete.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ func (c *CascadingDelete) Consume(ctx context.Context, envelope *messaging.Envel
9898
}
9999

100100
if c.resourceLabel != "" {
101-
opts.LabelSelector = labels.SelectorFromSet(map[string]string{
101+
opts.LabelSelector = labels.SelectorFromSet(labels.Set{
102102
c.resourceLabel: envelope.ResourceID,
103103
})
104104
}

pkg/openapi/common.spec.yaml

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,35 +17,45 @@ components:
1717
type: string
1818
schemas:
1919
error:
20-
description: Generic error message, compatible with oauth2.
20+
description: Represents a structured error response returned by the API. It consolidates both OAuth 2.0-related and general API errors into a unified format.
2121
type: object
2222
required:
23+
- type
24+
- status
2325
- error
24-
- error_description
2526
properties:
27+
type:
28+
description: Specifies whether the error originated from an OAuth 2.0 context or from the general API.
29+
type: string
30+
enum:
31+
- oauth2_error
32+
- api_error
33+
status:
34+
description: The HTTP status code returned with the response.
35+
type: integer
2636
error:
27-
description: A terse error string expanding on the HTTP error code. Errors are based on the OAuth 2.02 specification, but are expanded with proprietary status codes for APIs other than those specified by OAuth 2.02.
37+
description: A concise, machine-readable identifier for the specific error, enabling clients to handle particular conditions programmatically.
2838
type: string
2939
enum:
30-
# Defined by OAuth 2.02
40+
# Common error codes
3141
- invalid_request
32-
- unauthorized_client
42+
# OAuth 2.0 error codes
43+
- unsupported_grant_type
44+
- invalid_grant
45+
- invalid_token
46+
- invalid_client
3347
- access_denied
34-
- unsupported_response_type
35-
- invalid_scope
48+
- insufficient_scope
3649
- server_error
37-
- temporarily_unavailable
38-
- invalid_client
39-
- invalid_grant
40-
- unsupported_grant_type
41-
# Proprietary
42-
- not_found
50+
# API error codes
51+
- token_expired
52+
- unauthorized
53+
- resource_missing
4354
- conflict
44-
- method_not_allowed
45-
- unsupported_media_type
46-
- forbidden
55+
- quota_exhausted
56+
- internal
4757
error_description:
48-
description: Verbose message describing the error.
58+
description: A human-readable explanation that provides additional context about the error. This field is omitted when no extra description is available.
4959
type: string
5060
kubernetesLabelValue:
5161
description: |-

pkg/openapi/helpers.go

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,15 @@ limitations under the License.
1818
package openapi
1919

2020
import (
21+
"encoding/json"
22+
"fmt"
2123
"net/http"
2224

2325
"github.com/getkin/kin-openapi/openapi3"
2426
"github.com/getkin/kin-openapi/routers"
2527
"github.com/getkin/kin-openapi/routers/gorillamux"
2628

27-
"github.com/unikorn-cloud/core/pkg/server/errors"
29+
errorsv2 "github.com/unikorn-cloud/core/pkg/server/v2/errors"
2830
)
2931

3032
// Schema abstracts schema access and validation.
@@ -65,8 +67,55 @@ func NewSchema(get SchemaGetter) (*Schema, error) {
6567
func (s *Schema) FindRoute(r *http.Request) (*routers.Route, map[string]string, error) {
6668
route, params, err := s.router.FindRoute(r)
6769
if err != nil {
68-
return nil, nil, errors.OAuth2ServerError("unable to find route").WithError(err)
70+
err = fmt.Errorf("failed to find route: %w", err)
71+
return nil, nil, err
6972
}
7073

7174
return route, params, nil
7275
}
76+
77+
func parseJSONErrorResponse(headers http.Header, data []byte) error {
78+
var response errorsv2.Error
79+
if err := json.Unmarshal(data, &response); err != nil {
80+
return errorsv2.NewInternalError().WithCause(err).Prefixed()
81+
}
82+
83+
return response.WithSimpleCause("upstream error").
84+
WithWWWAuthenticate(headers).
85+
WithOAuth2ErrorCode(headers).
86+
WithAPIErrorCode(headers)
87+
}
88+
89+
//nolint:nlreturn,wsl
90+
func ParseJSONValueResponse[T any](headers http.Header, data []byte, status, expected int) (T, error) {
91+
if status != expected {
92+
var zero T
93+
err := parseJSONErrorResponse(headers, data)
94+
return zero, err
95+
}
96+
97+
var response T
98+
if err := json.Unmarshal(data, &response); err != nil {
99+
err = errorsv2.NewInternalError().WithCause(err).Prefixed()
100+
return response, err
101+
}
102+
103+
return response, nil
104+
}
105+
106+
//nolint:nlreturn,wsl
107+
func ParseJSONPointerResponse[T any](headers http.Header, data []byte, status, expected int) (*T, error) {
108+
if status != expected {
109+
var zero T
110+
err := parseJSONErrorResponse(headers, data)
111+
return &zero, err
112+
}
113+
114+
var response T
115+
if err := json.Unmarshal(data, &response); err != nil {
116+
err = errorsv2.NewInternalError().WithCause(err).Prefixed()
117+
return nil, err
118+
}
119+
120+
return &response, nil
121+
}

pkg/server/conversion/conversion.go

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
unikornv1 "github.com/unikorn-cloud/core/pkg/apis/unikorn/v1alpha1"
2929
"github.com/unikorn-cloud/core/pkg/constants"
3030
"github.com/unikorn-cloud/core/pkg/openapi"
31+
errorsv2 "github.com/unikorn-cloud/core/pkg/server/v2/errors"
3132
"github.com/unikorn-cloud/core/pkg/util"
3233

3334
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -165,7 +166,7 @@ func OrganizationScopedResourceReadMetadata(in metav1.Object, tags unikornv1.Tag
165166
return out
166167
}
167168

168-
// ProjectScopedResourceReadMetadata extracts project scoped metdata from a resource for
169+
// ProjectScopedResourceReadMetadata extracts project scoped metadata from a resource for
169170
// GET APIs.
170171
//
171172
//nolint:errchkjson
@@ -238,6 +239,16 @@ type MetadataMutationFunc func(required, current metav1.Object) error
238239

239240
// UpdateObjectMetadata abstracts away metadata updates.
240241
func UpdateObjectMetadata(required, current metav1.Object, mutators ...MetadataMutationFunc) error {
242+
if err := updateObjectMetadata(required, current, mutators); err != nil {
243+
return errorsv2.NewInternalError().
244+
WithCausef("failed to update object metadata: %w", err).
245+
Prefixed()
246+
}
247+
248+
return nil
249+
}
250+
251+
func updateObjectMetadata(required, current metav1.Object, mutators []MetadataMutationFunc) error {
241252
req := required.GetAnnotations()
242253
if req == nil {
243254
req = map[string]string{}
@@ -293,12 +304,10 @@ func LogUpdate(ctx context.Context, current, required metav1.Object) error {
293304
}
294305

295306
func ConvertTag(in unikornv1.Tag) openapi.Tag {
296-
out := openapi.Tag{
307+
return openapi.Tag{
297308
Name: in.Name,
298309
Value: in.Value,
299310
}
300-
301-
return out
302311
}
303312

304313
func ConvertTags(in unikornv1.TagList) openapi.TagList {
@@ -316,12 +325,10 @@ func ConvertTags(in unikornv1.TagList) openapi.TagList {
316325
}
317326

318327
func GenerateTag(in openapi.Tag) unikornv1.Tag {
319-
out := unikornv1.Tag{
328+
return unikornv1.Tag{
320329
Name: in.Name,
321330
Value: in.Value,
322331
}
323-
324-
return out
325332
}
326333

327334
func GenerateTagList(in *openapi.TagList) unikornv1.TagList {

pkg/server/handler/error.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
Copyright 2025 the Unikorn Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package handler
18+
19+
import (
20+
"net/http"
21+
22+
"github.com/unikorn-cloud/core/pkg/server/errors"
23+
)
24+
25+
// NotFound is called from the router when a path is not found.
26+
func NotFound(w http.ResponseWriter, r *http.Request) {
27+
errors.HTTPNotFound().Write(w, r)
28+
}
29+
30+
// MethodNotAllowed is called from the router when a method is not found for a path.
31+
func MethodNotAllowed(w http.ResponseWriter, r *http.Request) {
32+
errors.HTTPMethodNotAllowed().Write(w, r)
33+
}
34+
35+
// HandleError is called when the router has trouble parsing paths.
36+
func HandleError(w http.ResponseWriter, r *http.Request, err error) {
37+
errors.OAuth2InvalidRequest("invalid path/query element").WithError(err).Write(w, r)
38+
}

pkg/server/middleware/cors/cors.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ import (
2525
"github.com/spf13/pflag"
2626

2727
"github.com/unikorn-cloud/core/pkg/openapi"
28-
"github.com/unikorn-cloud/core/pkg/server/errors"
28+
errorsv2 "github.com/unikorn-cloud/core/pkg/server/v2/errors"
29+
"github.com/unikorn-cloud/core/pkg/server/v2/httputil"
2930
"github.com/unikorn-cloud/core/pkg/util"
3031
)
3132

@@ -65,7 +66,13 @@ func Middleware(schema *openapi.Schema, options *Options) func(http.Handler) htt
6566
// Handle preflight
6667
method := r.Header.Get("Access-Control-Request-Method")
6768
if method == "" {
68-
errors.HandleError(w, r, errors.OAuth2InvalidRequest("OPTIONS missing Access-Control-Request-Method header"))
69+
err := errorsv2.NewInvalidRequestError().
70+
WithSimpleCause("missing Access-Control-Request-Method header").
71+
WithErrorDescription("Missing Access-Control-Request-Method header in OPTIONS request.").
72+
Prefixed()
73+
74+
httputil.WriteErrorResponse(w, r, err)
75+
6976
return
7077
}
7178

@@ -74,7 +81,7 @@ func Middleware(schema *openapi.Schema, options *Options) func(http.Handler) htt
7481

7582
route, _, err := schema.FindRoute(request)
7683
if err != nil {
77-
errors.HandleError(w, r, err)
84+
httputil.WriteErrorResponse(w, r, err)
7885
return
7986
}
8087

pkg/server/util/json.go

Lines changed: 0 additions & 61 deletions
This file was deleted.

0 commit comments

Comments
 (0)