-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy patherrors.go
More file actions
135 lines (114 loc) · 3.6 KB
/
Copy patherrors.go
File metadata and controls
135 lines (114 loc) · 3.6 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
// Typed error hierarchy for NASA API responses.
package nasa
import (
"encoding/json"
"fmt"
"io"
"net/http"
"strconv"
"time"
)
const maxResponseBodyBytes = 10 << 20
func readResponseBody(r io.Reader) ([]byte, error) {
body, err := io.ReadAll(io.LimitReader(r, maxResponseBodyBytes+1))
if err != nil {
return nil, err
}
if len(body) > maxResponseBodyBytes {
return nil, fmt.Errorf("nasa: response body exceeds %d bytes", maxResponseBodyBytes)
}
return body, nil
}
func closeResponseBody(c io.Closer) {
_ = c.Close()
}
// Error is the base error type for all NASA API errors. It carries the HTTP
// status code, a human-readable message extracted from the response body, and
// an optional RetryAfter duration for rate-limited responses.
type Error struct {
StatusCode int
Message string
RetryAfter time.Duration
}
func (e *Error) Error() string {
if e.Message != "" {
return fmt.Sprintf("nasa: %d: %s", e.StatusCode, e.Message)
}
return fmt.Sprintf("nasa: %d: %s", e.StatusCode, http.StatusText(e.StatusCode))
}
// RateLimitError is returned when the API responds with HTTP 429 (Too Many
// Requests). The embedded [Error] includes a RetryAfter hint when the server
// provides one. Use errors.As to check for this type.
type RateLimitError struct {
Err *Error
}
func (e *RateLimitError) Error() string { return e.Err.Error() }
func (e *RateLimitError) Unwrap() error { return e.Err }
// AuthError is returned when the API responds with HTTP 401 (Unauthorized) or
// 403 (Forbidden), typically indicating an invalid or missing API key.
type AuthError struct {
Err *Error
}
func (e *AuthError) Error() string { return e.Err.Error() }
func (e *AuthError) Unwrap() error { return e.Err }
// NotFoundError is returned when the API responds with HTTP 404, indicating
// the requested resource does not exist (e.g., invalid date for APOD).
type NotFoundError struct {
Err *Error
}
func (e *NotFoundError) Error() string { return e.Err.Error() }
func (e *NotFoundError) Unwrap() error { return e.Err }
// APIError is returned for non-2xx HTTP responses that do not match the more
// specific error types (not 401, 403, 404, or 429).
type APIError struct {
Err *Error
}
func (e *APIError) Error() string { return e.Err.Error() }
func (e *APIError) Unwrap() error { return e.Err }
// newError reads the HTTP response and returns an appropriate typed error.
// The response body is read up to 10MB to prevent unbounded memory usage.
// The response body is closed by this function.
func newError(resp *http.Response) error {
defer closeResponseBody(resp.Body)
// Read body up to 10MB.
body, _ := readResponseBody(resp.Body)
msg := ""
// Try to extract message from JSON response body.
var apiResp struct {
Error struct {
Message string `json:"message"`
} `json:"error"`
Msg string `json:"msg"`
}
if json.Unmarshal(body, &apiResp) == nil {
if apiResp.Error.Message != "" {
msg = apiResp.Error.Message
} else if apiResp.Msg != "" {
msg = apiResp.Msg
}
}
if msg == "" {
msg = http.StatusText(resp.StatusCode)
}
base := &Error{
StatusCode: resp.StatusCode,
Message: msg,
}
// Parse Retry-After header for 429 responses.
if resp.StatusCode == http.StatusTooManyRequests {
if ra := resp.Header.Get("Retry-After"); ra != "" {
if seconds, err := strconv.Atoi(ra); err == nil {
base.RetryAfter = time.Duration(seconds) * time.Second
}
}
return &RateLimitError{Err: base}
}
switch resp.StatusCode {
case http.StatusUnauthorized, http.StatusForbidden:
return &AuthError{Err: base}
case http.StatusNotFound:
return &NotFoundError{Err: base}
default:
return &APIError{Err: base}
}
}