-
Notifications
You must be signed in to change notification settings - Fork 202
Add GitHub API rate limit handling UI #560
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -33,12 +33,78 @@ const ContributorsPage = () => { | |||||||||||||||
| useEffect(() => { | ||||||||||||||||
| const fetchContributors = async () => { | ||||||||||||||||
| try { | ||||||||||||||||
| const response = await axios.get(GITHUB_REPO_CONTRIBUTORS_URL, { | ||||||||||||||||
| withCredentials: false, | ||||||||||||||||
| }); | ||||||||||||||||
| // REAL API CALL | ||||||||||||||||
| const response = await axios.get( | ||||||||||||||||
| GITHUB_REPO_CONTRIBUTORS_URL, | ||||||||||||||||
| { | ||||||||||||||||
| withCredentials: false, | ||||||||||||||||
| } | ||||||||||||||||
| ); | ||||||||||||||||
|
|
||||||||||||||||
| setContributors(response.data); | ||||||||||||||||
| } catch { | ||||||||||||||||
| setError("Failed to fetch contributors. Please try again later."); | ||||||||||||||||
|
|
||||||||||||||||
| /* | ||||||||||||||||
| // ================================================== | ||||||||||||||||
| // TEMPORARY TESTING CODE | ||||||||||||||||
| // Uncomment this block ONLY if you want to test | ||||||||||||||||
| // the rate limit UI manually. | ||||||||||||||||
| // ================================================== | ||||||||||||||||
|
|
||||||||||||||||
| // // throw { | ||||||||||||||||
| // // isAxiosError: true, | ||||||||||||||||
| // // response: { | ||||||||||||||||
| // // status: 403, | ||||||||||||||||
| // // headers: { | ||||||||||||||||
| // // "x-ratelimit-remaining": 0, | ||||||||||||||||
| // // "x-ratelimit-reset": 1750000000, | ||||||||||||||||
| // // }, | ||||||||||||||||
| // // }, | ||||||||||||||||
| // // }; | ||||||||||||||||
|
|
||||||||||||||||
| // ================================================== | ||||||||||||||||
| // */ | ||||||||||||||||
| } catch (error) { | ||||||||||||||||
| if (axios.isAxiosError(error)) { | ||||||||||||||||
|
|
||||||||||||||||
| // GitHub API Rate Limit Error | ||||||||||||||||
| if (error.response?.status === 403) { | ||||||||||||||||
|
|
||||||||||||||||
| const remaining = | ||||||||||||||||
| error.response.headers["x-ratelimit-remaining"]; | ||||||||||||||||
|
|
||||||||||||||||
| const reset = | ||||||||||||||||
| error.response.headers["x-ratelimit-reset"]; | ||||||||||||||||
|
|
||||||||||||||||
| let resetMessage = ""; | ||||||||||||||||
|
|
||||||||||||||||
| if (reset) { | ||||||||||||||||
| const resetTime = new Date( | ||||||||||||||||
| Number(reset) * 1000 | ||||||||||||||||
| ); | ||||||||||||||||
|
|
||||||||||||||||
| resetMessage = | ||||||||||||||||
| ` Try again after ${resetTime.toLocaleTimeString()}.`; | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| setError( | ||||||||||||||||
| `GitHub API rate limit exceeded. Remaining requests: ${remaining}.${resetMessage}` | ||||||||||||||||
| ); | ||||||||||||||||
|
|
||||||||||||||||
| } else { | ||||||||||||||||
|
|
||||||||||||||||
| setError( | ||||||||||||||||
| "Failed to fetch contributors. Please try again later." | ||||||||||||||||
| ); | ||||||||||||||||
|
|
||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| } else { | ||||||||||||||||
|
|
||||||||||||||||
| setError( | ||||||||||||||||
| "An unexpected error occurred." | ||||||||||||||||
| ); | ||||||||||||||||
|
|
||||||||||||||||
| } | ||||||||||||||||
| } finally { | ||||||||||||||||
| setLoading(false); | ||||||||||||||||
| } | ||||||||||||||||
|
|
@@ -47,92 +113,129 @@ const ContributorsPage = () => { | |||||||||||||||
| fetchContributors(); | ||||||||||||||||
| }, []); | ||||||||||||||||
|
|
||||||||||||||||
| // Loading UI | ||||||||||||||||
| if (loading) { | ||||||||||||||||
| return ( | ||||||||||||||||
| <Box sx={{ display: "flex", justifyContent: "center", mt: 4 }}> | ||||||||||||||||
| <Box | ||||||||||||||||
| sx={{ | ||||||||||||||||
| display: "flex", | ||||||||||||||||
| justifyContent: "center", | ||||||||||||||||
| mt: 4, | ||||||||||||||||
| }} | ||||||||||||||||
| > | ||||||||||||||||
| <CircularProgress /> | ||||||||||||||||
| </Box> | ||||||||||||||||
| ); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| // Error UI | ||||||||||||||||
| if (error) { | ||||||||||||||||
| return ( | ||||||||||||||||
| <Box sx={{ mt: 4 }}> | ||||||||||||||||
| <Alert severity="error">{error}</Alert> | ||||||||||||||||
| <Alert severity="error"> | ||||||||||||||||
| {error} | ||||||||||||||||
| </Alert> | ||||||||||||||||
| </Box> | ||||||||||||||||
| ); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| return ( | ||||||||||||||||
| <div className="bg-white dark:bg-gray-900 text-black dark:text-white min-h-screen p-4 mt-4"> | ||||||||||||||||
| <Container> | ||||||||||||||||
| <Typography sx={{ pb: 2 }} variant="h4" align="center" gutterBottom> | ||||||||||||||||
| <Typography | ||||||||||||||||
| sx={{ pb: 2 }} | ||||||||||||||||
| variant="h4" | ||||||||||||||||
| align="center" | ||||||||||||||||
| gutterBottom | ||||||||||||||||
| > | ||||||||||||||||
| π€ Contributors | ||||||||||||||||
| </Typography> | ||||||||||||||||
|
|
||||||||||||||||
| <Grid container spacing={4}> | ||||||||||||||||
| {contributors.map((contributor) => ( | ||||||||||||||||
| <Grid item xs={12} sm={6} md={3} key={contributor.id}> | ||||||||||||||||
| <Card | ||||||||||||||||
| sx={{ | ||||||||||||||||
| textAlign: "center", | ||||||||||||||||
| p: 2, | ||||||||||||||||
| borderRadius: "10px", | ||||||||||||||||
| border: "1px solid #E0E0E0", | ||||||||||||||||
| boxShadow: "0 4px 10px rgba(0,0,0,0.1)", | ||||||||||||||||
| transition: "transform 0.3s ease-in-out", | ||||||||||||||||
| "&:hover": { | ||||||||||||||||
| transform: "scale(1.05)", | ||||||||||||||||
| boxShadow: "0 8px 15px rgba(0,0,0,0.2)", | ||||||||||||||||
| borderColor: "#C0C0C0", | ||||||||||||||||
| outlineColor: "#B3B3B3", | ||||||||||||||||
| }, | ||||||||||||||||
| <Grid | ||||||||||||||||
| item | ||||||||||||||||
| xs={12} | ||||||||||||||||
| sm={6} | ||||||||||||||||
| md={3} | ||||||||||||||||
| key={contributor.id} | ||||||||||||||||
| > | ||||||||||||||||
| <Card | ||||||||||||||||
| sx={{ | ||||||||||||||||
| textAlign: "center", | ||||||||||||||||
| p: 2, | ||||||||||||||||
| borderRadius: "10px", | ||||||||||||||||
| border: "1px solid #E0E0E0", | ||||||||||||||||
| boxShadow: | ||||||||||||||||
| "0 4px 10px rgba(0,0,0,0.1)", | ||||||||||||||||
| transition: | ||||||||||||||||
| "transform 0.3s ease-in-out", | ||||||||||||||||
|
|
||||||||||||||||
| "&:hover": { | ||||||||||||||||
| transform: "scale(1.05)", | ||||||||||||||||
| boxShadow: | ||||||||||||||||
| "0 8px 15px rgba(0,0,0,0.2)", | ||||||||||||||||
| borderColor: "#C0C0C0", | ||||||||||||||||
| outlineColor: "#B3B3B3", | ||||||||||||||||
| }, | ||||||||||||||||
| }} | ||||||||||||||||
| > | ||||||||||||||||
| <Link | ||||||||||||||||
| to={`/contributor/${contributor.login}`} | ||||||||||||||||
| style={{ | ||||||||||||||||
| textDecoration: "none", | ||||||||||||||||
| }} | ||||||||||||||||
| > | ||||||||||||||||
| <Link | ||||||||||||||||
| to={`/contributor/${contributor.login}`} | ||||||||||||||||
| style={{ textDecoration: "none" }} | ||||||||||||||||
| <Avatar | ||||||||||||||||
| src={contributor.avatar_url} | ||||||||||||||||
| alt={contributor.login} | ||||||||||||||||
| sx={{ | ||||||||||||||||
| width: 100, | ||||||||||||||||
| height: 100, | ||||||||||||||||
| mx: "auto", | ||||||||||||||||
| mb: 2, | ||||||||||||||||
| }} | ||||||||||||||||
| /> | ||||||||||||||||
|
|
||||||||||||||||
| <CardContent> | ||||||||||||||||
| <Typography | ||||||||||||||||
| variant="h6" | ||||||||||||||||
| sx={{ | ||||||||||||||||
| fontWeight: "bold", | ||||||||||||||||
| }} | ||||||||||||||||
| > | ||||||||||||||||
| {contributor.login} | ||||||||||||||||
| </Typography> | ||||||||||||||||
|
|
||||||||||||||||
| <Typography | ||||||||||||||||
| variant="body2" | ||||||||||||||||
| color="text.secondary" | ||||||||||||||||
| > | ||||||||||||||||
| <Avatar | ||||||||||||||||
| src={contributor.avatar_url} | ||||||||||||||||
| alt={contributor.login} | ||||||||||||||||
| sx={{ width: 100, height: 100, mx: "auto", mb: 2 }} | ||||||||||||||||
| /> | ||||||||||||||||
| <CardContent> | ||||||||||||||||
| <Typography variant="h6" sx={{ fontWeight: "bold" }}> | ||||||||||||||||
| {contributor.login} | ||||||||||||||||
| </Typography> | ||||||||||||||||
|
|
||||||||||||||||
| <Typography variant="body2" color="text.secondary"> | ||||||||||||||||
| {contributor.contributions} Contributions | ||||||||||||||||
| </Typography> | ||||||||||||||||
| {/* | ||||||||||||||||
| <Typography variant="body2" sx={{ mt: 2 }}> | ||||||||||||||||
| Thank you for your valuable contributions to our | ||||||||||||||||
| community! | ||||||||||||||||
| </Typography> */} | ||||||||||||||||
| </CardContent> | ||||||||||||||||
| </Link> | ||||||||||||||||
|
|
||||||||||||||||
| <Box sx={{ mt: 2 }}> | ||||||||||||||||
| <Button | ||||||||||||||||
| variant="contained" | ||||||||||||||||
| startIcon={<FaGithub />} | ||||||||||||||||
| href={contributor.html_url} | ||||||||||||||||
| target="_blank" | ||||||||||||||||
| sx={{ | ||||||||||||||||
| backgroundColor: "#333333", | ||||||||||||||||
| textTransform: "none", | ||||||||||||||||
| color: "#FFFFFF", | ||||||||||||||||
| "&:hover": { | ||||||||||||||||
| backgroundColor: "#555555", | ||||||||||||||||
| }, | ||||||||||||||||
| }} | ||||||||||||||||
| > | ||||||||||||||||
| GitHub | ||||||||||||||||
| </Button> | ||||||||||||||||
| </Box> | ||||||||||||||||
| </Card> | ||||||||||||||||
| {contributor.contributions} Contributions | ||||||||||||||||
| </Typography> | ||||||||||||||||
| </CardContent> | ||||||||||||||||
| </Link> | ||||||||||||||||
|
|
||||||||||||||||
| <Box sx={{ mt: 2 }}> | ||||||||||||||||
| <Button | ||||||||||||||||
| variant="contained" | ||||||||||||||||
| startIcon={<FaGithub />} | ||||||||||||||||
| href={contributor.html_url} | ||||||||||||||||
| target="_blank" | ||||||||||||||||
| sx={{ | ||||||||||||||||
|
Comment on lines
+223
to
+225
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add Line 224 uses Proposed fix <Button
variant="contained"
startIcon={<FaGithub />}
href={contributor.html_url}
target="_blank"
+ rel="noopener noreferrer"
sx={{π Committable suggestion
Suggested change
π€ Prompt for AI Agents |
||||||||||||||||
| backgroundColor: "#333333", | ||||||||||||||||
| textTransform: "none", | ||||||||||||||||
| color: "#FFFFFF", | ||||||||||||||||
|
|
||||||||||||||||
| "&:hover": { | ||||||||||||||||
| backgroundColor: "#555555", | ||||||||||||||||
| }, | ||||||||||||||||
| }} | ||||||||||||||||
| > | ||||||||||||||||
| GitHub | ||||||||||||||||
| </Button> | ||||||||||||||||
| </Box> | ||||||||||||||||
| </Card> | ||||||||||||||||
| </Grid> | ||||||||||||||||
| ))} | ||||||||||||||||
| </Grid> | ||||||||||||||||
|
|
@@ -141,4 +244,4 @@ const ContributorsPage = () => { | |||||||||||||||
| ); | ||||||||||||||||
| }; | ||||||||||||||||
|
|
||||||||||||||||
| export default ContributorsPage; | ||||||||||||||||
| export default ContributorsPage; | ||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
π§© Analysis chain
π Web query:
In GitHub REST API, are all HTTP 403 responses considered rate-limit errors? What is the recommended way to distinguish rate-limit responses (e.g., x-ratelimit-remaining/reset headers and response message) from other forbidden responses?π‘ Result:
No, not all HTTP 403 (Forbidden) responses from the GitHub REST API are caused by rate-limit errors [1][2]. A 403 status code is a general response indicating that the server refuses to authorize the request, which can occur due to insufficient permissions, invalid credentials, or other access-related issues, in addition to both primary and secondary rate limiting [1][3][4]. To distinguish a rate-limit error from other types of 403 Forbidden responses, you should examine the response body and specific HTTP headers [1][3][5]. Recommended approach to identify and handle rate limits: 1. Check the response body: GitHub explicitly includes information in the error message when a rate limit has been exceeded [1][3]. For example, messages regarding secondary rate limits will explicitly state that a secondary rate limit was exceeded [1][3]. 2. Check for the Retry-After header: If this header is present, it is a strong indicator that you have hit a rate limit (typically a secondary rate limit), and the value specifies how many seconds you must wait before retrying [1][3][5]. 3. Check x-ratelimit-remaining: If the x-ratelimit-remaining header is 0, it confirms that your primary rate limit has been exhausted [1][3][5]. The x-ratelimit-reset header will provide the UTC epoch time when your limit will reset [1][3][5]. If a 403 response does not contain these specific rate-limit indicators, it is likely a standard authorization or permission error [2][6][4]. In such cases, retrying will not resolve the issue and may be considered abusive behavior [1][5]. For rate-limit errors, follow the prescribed backoff strategiesβsuch as waiting until the time specified by the x-ratelimit-reset header or using exponential backoff if a retry-after header is presentβto remain compliant and avoid potential banning of your integration [1][3][5].
Citations:
Avoid mislabeling all GitHub 403s as rate limits (and prevent
undefinedin error text)At
src/pages/Contributors/Contributors.tsx(lines 70-91), the code treats every403as a rate-limit issue, but GitHub can return403for many non-rate-limit authorization/permission problems. Also, it interpolatesx-ratelimit-remainingdirectly, which can showundefinedto users when the header isnβt present.Proposed hardening
π Committable suggestion
π€ Prompt for AI Agents