This repository was archived by the owner on Mar 17, 2026. It is now read-only.
-
-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathapi.go
More file actions
190 lines (155 loc) · 4.86 KB
/
api.go
File metadata and controls
190 lines (155 loc) · 4.86 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
package api
import (
"bytes"
"fmt"
"io"
"net/http"
"net/http/httputil"
"net/url"
"os"
"github.com/nitrictech/suga/cli/internal/config"
"github.com/nitrictech/suga/cli/internal/utils"
"github.com/pkg/errors"
"github.com/samber/do/v2"
)
type TokenProvider interface {
// GetAccessToken returns the access token for the user
GetAccessToken(forceRefresh bool) (string, error)
}
type SugaApiClient struct {
tokenProvider TokenProvider
apiUrl *url.URL
debugEnabled bool
}
func NewSugaApiClient(injector do.Injector) (*SugaApiClient, error) {
config, err := do.Invoke[*config.Config](injector)
if err != nil {
return nil, fmt.Errorf("failed to get config: %w", err)
}
apiUrl := config.GetSugaServerUrl()
tokenProvider, err := do.InvokeAs[TokenProvider](injector)
if err != nil {
return nil, fmt.Errorf("failed to get token provider: %w", err)
}
return &SugaApiClient{
apiUrl: apiUrl,
tokenProvider: tokenProvider,
debugEnabled: config.Debug,
}, nil
}
// logRequest logs the HTTP request details if debug mode is enabled
func (c *SugaApiClient) logRequest(req *http.Request) {
if !c.debugEnabled {
return
}
fmt.Fprintf(os.Stderr, "[DEBUG] API Request:\n")
if dump, err := httputil.DumpRequest(req, true); err == nil {
fmt.Fprintf(os.Stderr, "%s\n\n", dump)
}
}
// logResponse logs the HTTP response details if debug mode is enabled
func (c *SugaApiClient) logResponse(resp *http.Response) {
if !c.debugEnabled {
return
}
fmt.Fprintf(os.Stderr, "[DEBUG] API Response:\n")
if dump, err := httputil.DumpResponse(resp, true); err == nil {
fmt.Fprintf(os.Stderr, "%s\n\n", dump)
}
}
// doRequestWithRetry executes an HTTP request and retries once with a refreshed token on 401/403.
// Reuses req.Context() and req.GetBody (when available) to rebuild the body.
func (c *SugaApiClient) doRequestWithRetry(req *http.Request, requiresAuth bool) (*http.Response, error) {
if requiresAuth {
if c.tokenProvider == nil {
return nil, errors.Wrap(ErrPreconditionFailed, "no token provider provided")
}
// First attempt with existing token
token, err := c.tokenProvider.GetAccessToken(false)
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrUnauthenticated, err)
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
}
// Log the request
c.logRequest(req)
// Execute the request
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
// Log the response
c.logResponse(resp)
// If we got a 401 or 403 and auth is required, try refreshing the token
if requiresAuth && (resp.StatusCode == 401 || resp.StatusCode == 403) {
resp.Body.Close() // Close the first response body
// Force token refresh
token, err := c.tokenProvider.GetAccessToken(true)
if err != nil {
return nil, fmt.Errorf("%w: token refresh failed: %v", ErrUnauthenticated, err)
}
// Clone the request for retry - use GetBody to recreate the body if available
var bodyReader io.Reader
var retryBodyRC io.ReadCloser
// Cannot safely retry request with a consumed, non-rewindable body
if req.Body != nil && req.GetBody == nil && req.ContentLength != 0 {
return nil, fmt.Errorf("%w: cannot retry request with non-rewindable body", ErrUnauthenticated)
}
if req.GetBody != nil {
var err2 error
retryBodyRC, err2 = req.GetBody()
if err2 != nil {
return nil, err2
}
bodyReader = retryBodyRC
}
retryReq, err := http.NewRequestWithContext(req.Context(), req.Method, req.URL.String(), bodyReader)
if err != nil {
if retryBodyRC != nil {
_ = retryBodyRC.Close()
}
return nil, err
}
// Copy headers
retryReq.Header = req.Header.Clone()
// Update authorization header with new token
retryReq.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
// Log the retry request
c.logRequest(retryReq)
// Retry the request
retryResp, err := http.DefaultClient.Do(retryReq)
if err != nil {
return nil, err
}
// Log the retry response
c.logResponse(retryResp)
return retryResp, nil
}
return resp, nil
}
func (c *SugaApiClient) get(path string, requiresAuth bool) (*http.Response, error) {
apiUrl, err := url.JoinPath(c.apiUrl.String(), path)
if err != nil {
return nil, err
}
req, err := http.NewRequest("GET", apiUrl, nil)
if err != nil {
return nil, err
}
req.Header.Set("Accept", "application/json")
return c.doRequestWithRetry(req, requiresAuth)
}
func (c *SugaApiClient) post(path string, requiresAuth bool, body []byte) (*http.Response, error) {
apiUrl, err := url.JoinPath(c.apiUrl.String(), path)
if err != nil {
return nil, err
}
req, err := http.NewRequest("POST", apiUrl, bytes.NewReader(body))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
req.Header.Set("x-amz-content-sha256", utils.CalculateSHA256(body))
return c.doRequestWithRetry(req, requiresAuth)
}