forked from anomalyco/opencode-sdk-go
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy patherrors.go
More file actions
306 lines (270 loc) · 9.38 KB
/
errors.go
File metadata and controls
306 lines (270 loc) · 9.38 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
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
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
package opencode
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"
"unicode"
"unicode/utf8"
)
var (
ErrNotFound = errors.New("resource not found")
ErrUnauthorized = errors.New("unauthorized")
ErrForbidden = errors.New("forbidden")
ErrRateLimited = errors.New("rate limited")
ErrInvalidRequest = errors.New("invalid request")
// ErrMissingRequiredParameter matches input validation failures where a
// required API parameter is not provided.
ErrMissingRequiredParameter = errors.New("missing required parameter")
// ErrParamsRequired matches validation failures where a request params
// object is required but nil was passed.
ErrParamsRequired = errors.New("params is required")
// ErrRequiredField matches validation failures where a required field on a
// provided struct is empty.
ErrRequiredField = errors.New("required field missing")
// ErrContextRequired is returned when a nil context.Context is passed to
// an API method that performs HTTP requests.
ErrContextRequired = errors.New("context is required")
// ErrTimeout matches HTTP 408 (Request Timeout) responses from the server.
// Client-side timeouts from context.WithTimeout or context.WithDeadline
// surface as context.DeadlineExceeded and are NOT matched by this sentinel.
// Use errors.Is(err, context.DeadlineExceeded) to detect client-side timeouts.
ErrTimeout = errors.New("request timeout")
ErrInternal = errors.New("internal server error")
// ErrWrongVariant is returned when a union type accessor is called with
// a discriminator value that does not match the requested variant.
ErrWrongVariant = errors.New("wrong union variant")
// ErrNilAuth is returned when AuthSetParams.MarshalJSON is called with a nil
// Auth field or a non-nil interface holding a nil pointer.
ErrNilAuth = errors.New("nil auth value")
// ErrUnknownAuthType is returned when AuthSetParams.MarshalJSON encounters
// an Auth implementation that is not one of OAuth, ApiAuth, or WellKnownAuth.
ErrUnknownAuthType = errors.New("unknown auth union type")
)
const maxAPIErrorMessageLength = 512
type MissingRequiredParameterError struct {
Parameter string
}
func (e *MissingRequiredParameterError) Error() string {
return fmt.Sprintf("missing required %s parameter", e.Parameter)
}
func (e *MissingRequiredParameterError) Is(target error) bool {
if target == ErrMissingRequiredParameter {
return true
}
t, ok := target.(*MissingRequiredParameterError)
if !ok {
return false
}
return t.Parameter == "" || t.Parameter == e.Parameter
}
func missingRequiredParameterError(parameter string) error {
return &MissingRequiredParameterError{Parameter: parameter}
}
type RequiredFieldError struct {
Field string
}
func (e *RequiredFieldError) Error() string {
return fmt.Sprintf("%s is required", e.Field)
}
func (e *RequiredFieldError) Is(target error) bool {
if target == ErrRequiredField {
return true
}
t, ok := target.(*RequiredFieldError)
if !ok {
return false
}
return t.Field == "" || t.Field == e.Field
}
func requiredFieldError(field string) error {
return &RequiredFieldError{Field: field}
}
func wrongVariant(expected, actual string) error {
return fmt.Errorf("%s, got %s: %w", expected, actual, ErrWrongVariant)
}
type APIError struct {
StatusCode int
Message string
RequestID string
Body string
// Truncated is true when the response body exceeded the read limit
// and Body contains only the first portion of the original response.
Truncated bool
// ReadErr is non-nil when the response body could not be fully read
// (e.g. connection dropped mid-transfer). Body contains whatever
// partial data was received before the error.
ReadErr error
}
func (e *APIError) Error() string {
var msg string
if e.RequestID != "" {
msg = fmt.Sprintf("%s (status %d, request %s)", e.Message, e.StatusCode, e.RequestID)
} else {
msg = fmt.Sprintf("%s (status %d)", e.Message, e.StatusCode)
}
if e.ReadErr != nil {
msg += fmt.Sprintf(" (body read error: %v)", e.ReadErr)
}
return msg
}
func isRetryableStatus(code int) bool {
return code == http.StatusRequestTimeout ||
code == http.StatusTooManyRequests ||
code >= http.StatusInternalServerError
}
// IsRetryable reports whether the error represents a transient failure
// (408 Request Timeout, 429 Too Many Requests, or 5xx) that may succeed
// on retry. Callers implementing SSE reconnection or custom retry logic
// can use this to distinguish transient from permanent failures.
func (e *APIError) IsRetryable() bool {
return isRetryableStatus(e.StatusCode)
}
func (e *APIError) Is(target error) bool {
switch {
case e.StatusCode == http.StatusNotFound:
return target == ErrNotFound
case e.StatusCode == http.StatusUnauthorized:
return target == ErrUnauthorized
case e.StatusCode == http.StatusForbidden:
return target == ErrForbidden
case e.StatusCode == http.StatusTooManyRequests:
return target == ErrRateLimited
case e.StatusCode == http.StatusRequestTimeout:
return target == ErrTimeout
case e.StatusCode >= http.StatusBadRequest && e.StatusCode < http.StatusInternalServerError:
return target == ErrInvalidRequest
case e.StatusCode >= http.StatusInternalServerError:
return target == ErrInternal
}
return false
}
// IsRetryableError reports whether err wraps an *APIError with a retryable
// status code (408, 429, or 5xx).
//
// Transport-level failures (DNS resolution, connection refused, TLS handshake
// errors, etc.) are NOT wrapped as *APIError, so this function returns false
// for them. To detect transport errors after retries are exhausted, unwrap the
// underlying net.Error or check for specific error types directly.
func IsRetryableError(err error) bool {
var apiErr *APIError
if errors.As(err, &apiErr) {
return apiErr.IsRetryable()
}
return false
}
// readAPIError reads the response body (up to limit bytes), constructs an
// *APIError, and closes the body. The caller should not use resp.Body after.
func readAPIError(resp *http.Response, bodyLimit int64) *APIError {
// Read one extra byte beyond the limit to detect truncation.
bodyBytes, readErr := io.ReadAll(io.LimitReader(resp.Body, bodyLimit+1))
_ = resp.Body.Close()
truncated := int64(len(bodyBytes)) > bodyLimit
if truncated {
bodyBytes = bodyBytes[:bodyLimit]
}
body := string(bodyBytes)
msg := apiErrorMessage(resp.StatusCode, body)
return &APIError{
StatusCode: resp.StatusCode,
Message: msg,
RequestID: resp.Header.Get("X-Request-Id"),
Body: body,
Truncated: truncated,
ReadErr: readErr,
}
}
func apiErrorMessage(statusCode int, body string) string {
if candidate := apiErrorMessageFromBody(body); candidate != "" {
return candidate
}
return apiErrorStatusText(statusCode)
}
func apiErrorStatusText(statusCode int) string {
msg := http.StatusText(statusCode)
if msg == "" {
return fmt.Sprintf("http %d", statusCode)
}
return msg
}
func apiErrorMessageFromBody(body string) string {
trimmed := strings.TrimSpace(body)
if trimmed == "" {
return ""
}
if fromJSON := apiErrorMessageFromJSON(trimmed); fromJSON != "" {
return sanitizeAPIErrorMessage(fromJSON)
}
return sanitizeAPIErrorMessage(trimmed)
}
func apiErrorMessageFromJSON(raw string) string {
var payload any
if err := json.Unmarshal([]byte(raw), &payload); err != nil {
return ""
}
return findMessageString(payload)
}
func findMessageString(value any) string {
switch v := value.(type) {
case map[string]any:
for _, key := range []string{"message", "error", "detail", "title", "reason", "description"} {
if field, ok := v[key]; ok {
if msg := findMessageString(field); msg != "" {
return msg
}
}
}
case []any:
for _, item := range v {
if msg := findMessageString(item); msg != "" {
return msg
}
}
case string:
return strings.TrimSpace(v)
}
return ""
}
func sanitizeAPIErrorMessage(msg string) string {
if msg == "" {
return ""
}
var cleaned strings.Builder
for _, r := range msg {
if !unicode.IsPrint(r) {
continue
}
if unicode.IsSpace(r) {
cleaned.WriteByte(' ')
continue
}
cleaned.WriteRune(r)
}
normalized := strings.Join(strings.Fields(cleaned.String()), " ")
if normalized == "" {
return ""
}
if utf8.RuneCountInString(normalized) <= maxAPIErrorMessageLength {
return normalized
}
runes := []rune(normalized)
return string(runes[:maxAPIErrorMessageLength-3]) + "..."
}
// IsTimeoutError reports whether err matches an HTTP 408 (Request Timeout)
// response. It does not match client-side timeouts caused by context
// cancellation or deadlines — use errors.Is(err, context.DeadlineExceeded)
// for those.
//
// All Is*Error helpers only match errors that wrap *APIError (HTTP responses).
// Transport-level failures (DNS, connection refused, TLS errors) are returned
// as plain errors and will not match any of these helpers. Use errors.As with
// net.Error or unwrap the error directly to classify transport failures.
func IsTimeoutError(err error) bool { return errors.Is(err, ErrTimeout) }
func IsNotFoundError(err error) bool { return errors.Is(err, ErrNotFound) }
func IsUnauthorizedError(err error) bool { return errors.Is(err, ErrUnauthorized) }
func IsForbiddenError(err error) bool { return errors.Is(err, ErrForbidden) }
func IsRateLimitedError(err error) bool { return errors.Is(err, ErrRateLimited) }
func IsInvalidRequestError(err error) bool { return errors.Is(err, ErrInvalidRequest) }
func IsInternalError(err error) bool { return errors.Is(err, ErrInternal) }