Repository navigation
Expand file tree
/
Copy patherrors.go
More file actions
159 lines (142 loc) · 5.16 KB
/
Copy patherrors.go
File metadata and controls
159 lines (142 loc) · 5.16 KB
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
// Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package server
import (
"errors"
"maps"
"net/http"
"time"
aicrerrors "github.com/NVIDIA/aicr/pkg/errors"
"github.com/NVIDIA/aicr/pkg/serializer"
"github.com/google/uuid"
)
// errorResponse represents error responses as per OpenAPI spec
type errorResponse struct {
Code string `json:"code" yaml:"code"`
Message string `json:"message" yaml:"message"`
Details map[string]any `json:"details,omitempty" yaml:"details,omitempty"`
RequestID string `json:"requestId" yaml:"requestId"`
Timestamp time.Time `json:"timestamp" yaml:"timestamp"`
Retryable bool `json:"retryable" yaml:"retryable"`
}
// writeError writes error response
func WriteError(w http.ResponseWriter, r *http.Request, statusCode int,
code aicrerrors.ErrorCode, message string, retryable bool, details map[string]any) {
requestID, _ := r.Context().Value(contextKeyRequestID).(string)
if requestID == "" {
requestID = uuid.New().String()
}
errResp := errorResponse{
Code: string(code),
Message: message,
Details: details,
RequestID: requestID,
Timestamp: time.Now().UTC(),
Retryable: retryable,
}
serializer.RespondJSON(w, statusCode, errResp)
}
// httpStatusFromCode maps a canonical error code to an HTTP status.
// This keeps transport-layer semantics centralized.
func httpStatusFromCode(code aicrerrors.ErrorCode) int {
switch code {
case aicrerrors.ErrCodeInvalidRequest:
return http.StatusBadRequest
case aicrerrors.ErrCodeUnauthorized:
return http.StatusUnauthorized
case aicrerrors.ErrCodeNotFound:
return http.StatusNotFound
case aicrerrors.ErrCodeMethodNotAllowed:
return http.StatusMethodNotAllowed
case aicrerrors.ErrCodeRateLimitExceeded:
return http.StatusTooManyRequests
case aicrerrors.ErrCodeUnavailable:
return http.StatusServiceUnavailable
case aicrerrors.ErrCodeTimeout:
// Prefer 504 for upstream timeouts and internal deadline exceeded.
return http.StatusGatewayTimeout
case aicrerrors.ErrCodeConflict:
return http.StatusConflict
case aicrerrors.ErrCodeCanceled:
// Client-closed-request semantics; 499 is non-standard, so the
// closest standard code for "the request was abandoned" is used.
// This code originates in CLI paths and should not reach a handler.
return http.StatusRequestTimeout
case aicrerrors.ErrCodeInternal:
fallthrough
default:
return http.StatusInternalServerError
}
}
func retryableFromCode(code aicrerrors.ErrorCode) bool {
switch code {
case aicrerrors.ErrCodeInvalidRequest,
aicrerrors.ErrCodeUnauthorized,
aicrerrors.ErrCodeNotFound,
aicrerrors.ErrCodeMethodNotAllowed,
aicrerrors.ErrCodeConflict,
// A deliberate abort is not retryable: the caller asked to stop.
aicrerrors.ErrCodeCanceled:
return false
case aicrerrors.ErrCodeTimeout,
aicrerrors.ErrCodeUnavailable,
aicrerrors.ErrCodeRateLimitExceeded,
aicrerrors.ErrCodeInternal:
return true
}
// Defensive fallback (should be unreachable if codes are kept in sync).
return false
}
func mergeDetails(a, b map[string]any) map[string]any {
if len(a) == 0 && len(b) == 0 {
return nil
}
out := make(map[string]any, len(a)+len(b))
maps.Copy(out, a)
maps.Copy(out, b)
return out
}
// WriteErrorFromErr writes an ErrorResponse based on a canonical structured error.
// If err is not a *errors.StructuredError, it falls back to INTERNAL.
//
// The underlying cause string is only embedded in the response details for
// 4xx errors (where the cause is typically validator output the client
// needs). For 5xx errors the cause is logged but withheld from the
// response to avoid leaking internal paths, kubeconfig contents, or
// service hostnames to remote clients.
func WriteErrorFromErr(w http.ResponseWriter, r *http.Request, err error, fallbackMessage string, extraDetails map[string]any) {
if err == nil {
WriteError(w, r, http.StatusInternalServerError, aicrerrors.ErrCodeInternal,
fallbackMessage, true, extraDetails)
return
}
if se, ok := errors.AsType[*aicrerrors.StructuredError](err); ok {
msg := se.Message
if msg == "" {
msg = fallbackMessage
}
status := httpStatusFromCode(se.Code)
details := mergeDetails(se.Context, extraDetails)
if se.Cause != nil {
if status < http.StatusInternalServerError {
details = mergeDetails(details, map[string]any{"error": se.Cause.Error()})
}
}
WriteError(w, r, status, se.Code, msg, retryableFromCode(se.Code), details)
return
}
// Unstructured error → 500. Do not leak Error() text to clients.
WriteError(w, r, http.StatusInternalServerError, aicrerrors.ErrCodeInternal,
fallbackMessage, true, extraDetails)
}