Skip to content

Commit 3969e40

Browse files
committed
✨ manage github rate limiting
1 parent c37a6b2 commit 3969e40

2 files changed

Lines changed: 123 additions & 29 deletions

File tree

internal/stars.go

Lines changed: 63 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,12 @@ const GRAPHQL_TEMPLATE = `
6262
}
6363
}
6464
}
65+
rateLimit {
66+
limit
67+
remaining
68+
used
69+
resetAt
70+
}
6571
}
6672
`
6773

@@ -100,6 +106,13 @@ type GhLanguageNode struct {
100106
Name string `json:"name"`
101107
}
102108

109+
type RateLimit struct {
110+
Limit int `json:"limit"`
111+
Remaining int `json:"remaining"`
112+
Used int `json:"used"`
113+
ResetAt string `json:"resetAt"`
114+
}
115+
103116
type GhQuery struct {
104117
Data struct {
105118
Viewer struct {
@@ -111,6 +124,7 @@ type GhQuery struct {
111124
} `json:"pageInfo"`
112125
} `json:"starredRepositories"`
113126
} `json:"viewer"`
127+
RateLimit RateLimit `json:"rateLimit"`
114128
} `json:"data"`
115129
}
116130

@@ -132,6 +146,8 @@ func (ghs *GhStarsService) GetStaredRepos(ctx context.Context) ([]Repo, error) {
132146
return nil, err
133147
}
134148

149+
log.Debug("Remaining rate limit", "remaining", response.Data.RateLimit.Remaining)
150+
135151
result = append(result, mapGhQueryToHelpWantedIssue(response)...)
136152

137153
if response.Data.Viewer.StarredRepositories.PageInfo.HasNextPage {
@@ -199,41 +215,14 @@ func (ghs *GhStarsService) fetchQueryResults(ctx context.Context, cursor string)
199215
req.Header.Set("Content-Type", "application/json")
200216

201217
// Send the request
202-
resp, err := doWithRetry(httpClient, req)
218+
ghResponse, err := doWithRetry(httpClient, req)
203219
if err != nil {
204220
log.Error("Error sending request: %v", err)
205221

206222
return GhQuery{}, err
207223
}
208-
defer closeBody(resp.Body)
209224

210-
return processResponse(resp)
211-
}
212-
213-
func processResponse(resp *http.Response) (GhQuery, error) {
214-
if resp.StatusCode != http.StatusOK {
215-
log.Error("Error sending request", "status", resp.Status)
216-
217-
return GhQuery{}, ErrUnexpectedStatusCode
218-
}
219-
220-
// Read the response
221-
body, err := io.ReadAll(resp.Body)
222-
if err != nil {
223-
log.Error("Error reading response: %v", err)
224-
225-
return GhQuery{}, fmt.Errorf("failed to read response: %w", err)
226-
}
227-
228-
var queryResult GhQuery
229-
230-
if err = json.Unmarshal(body, &queryResult); err != nil {
231-
log.Error("Error unmarshaling response: %v", err)
232-
233-
return GhQuery{}, fmt.Errorf("failed to unmarshal response: %w", err)
234-
}
235-
236-
return queryResult, nil
225+
return responseToResult(ghResponse)
237226
}
238227

239228
func doWithRetry(httpclient *http.Client, req *http.Request) (*http.Response, error) {
@@ -245,6 +234,22 @@ func doWithRetry(httpclient *http.Client, req *http.Request) (*http.Response, er
245234
return resp, nil
246235
}
247236

237+
if resp.StatusCode == http.StatusTooManyRequests {
238+
log.Warn("Rate limit exceeded, wait until reset...")
239+
240+
queryRes, err := responseToResult(resp)
241+
if err != nil {
242+
return nil, err
243+
}
244+
245+
resetTime, err := ParseGhDate(queryRes.Data.RateLimit.ResetAt)
246+
if err != nil {
247+
return nil, err
248+
}
249+
// Wait until the rate limit is reset
250+
time.Sleep(time.Until(resetTime))
251+
}
252+
248253
if err == nil {
249254
log.Warn("Github server error", "status", resp.StatusCode)
250255
}
@@ -258,8 +263,37 @@ func doWithRetry(httpclient *http.Client, req *http.Request) (*http.Response, er
258263
}
259264
}
260265

266+
func responseToResult(resp *http.Response) (GhQuery, error) {
267+
queryResult := GhQuery{}
268+
269+
body, err := io.ReadAll(resp.Body)
270+
if err != nil {
271+
log.Errorf("Error reading rate limited body : %v", err)
272+
273+
return queryResult, fmt.Errorf("failed to read response: %w", err)
274+
}
275+
defer closeBody(resp.Body)
276+
277+
if err = json.Unmarshal(body, &queryResult); err != nil {
278+
log.Error("Error unmarshaling response: %v", err)
279+
280+
return queryResult, fmt.Errorf("failed to unmarshal response: %w", err)
281+
}
282+
283+
return queryResult, nil
284+
}
285+
261286
func closeBody(body io.ReadCloser) {
262287
if err := body.Close(); err != nil {
263288
log.Warn("Error closing response body: %v", err)
264289
}
265290
}
291+
292+
func ParseGhDate(date string) (time.Time, error) {
293+
t, err := time.Parse(time.RFC3339, date)
294+
if err != nil {
295+
return time.Time{}, fmt.Errorf("failed to parse date: %w", err)
296+
}
297+
298+
return t, nil
299+
}

internal_test/stars_test.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package internal_test
2+
3+
import (
4+
"help-the-stars/internal"
5+
"testing"
6+
"time"
7+
)
8+
9+
func TestParseDate(t *testing.T) {
10+
t.Parallel()
11+
12+
tests := []struct {
13+
name string
14+
date string
15+
expected time.Time
16+
wantErr bool
17+
}{
18+
{
19+
name: "Valid RFC3339 date",
20+
date: "2026-02-14T10:26:00Z",
21+
expected: time.Date(2026, time.February, 14, 10, 26, 0, 0, time.UTC),
22+
wantErr: false,
23+
},
24+
{
25+
name: "Empty date string",
26+
date: "",
27+
expected: time.Time{},
28+
wantErr: true,
29+
},
30+
{
31+
name: "Invalid date format",
32+
date: "2026-02-14",
33+
expected: time.Time{},
34+
wantErr: true,
35+
},
36+
{
37+
name: "Malformed date string",
38+
date: "not-a-date",
39+
expected: time.Time{},
40+
wantErr: true,
41+
},
42+
}
43+
44+
for _, tt := range tests {
45+
t.Run(tt.name, func(t *testing.T) {
46+
t.Parallel()
47+
48+
got, err := internal.ParseGhDate(tt.date)
49+
if (err != nil) != tt.wantErr {
50+
t.Errorf("ParseDate() error = %v, wantErr %v", err, tt.wantErr)
51+
52+
return
53+
}
54+
55+
if !tt.wantErr && !got.Equal(tt.expected) {
56+
t.Errorf("ParseDate() = %v, want %v", got, tt.expected)
57+
}
58+
})
59+
}
60+
}

0 commit comments

Comments
 (0)