Skip to content

Commit f09387a

Browse files
Merge pull request #532 from Tanayajadhav1/fix/contributors-response-validation
fix: validate Contributors API response schema
2 parents 1bd3621 + 61c37f9 commit f09387a

1 file changed

Lines changed: 107 additions & 6 deletions

File tree

src/pages/Contributors/Contributors.tsx

Lines changed: 107 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,45 @@ interface Contributor {
2525
html_url: string;
2626
}
2727

28+
interface FetchError {
29+
message: string;
30+
isRateLimited: boolean;
31+
statusCode?: number;
32+
}
33+
34+
// Custom error class for Contributors fetch errors
35+
class ContributorsError extends Error {
36+
constructor(
37+
message: string,
38+
public isRateLimited = false,
39+
public statusCode?: number
40+
) {
41+
super(message);
42+
this.name = "ContributorsError";
43+
}
44+
}
45+
46+
// Type guard to validate if data is Contributor[]
47+
const isContributorArray = (data: unknown): data is Contributor[] => {
48+
if (!Array.isArray(data)) return false;
49+
return data.every((item) => {
50+
if (typeof item !== "object" || item === null) return false;
51+
52+
// Validate all required fields with correct types
53+
return (
54+
typeof item.id === "number" &&
55+
typeof item.login === "string" &&
56+
typeof item.avatar_url === "string" &&
57+
typeof item.contributions === "number" &&
58+
typeof item.html_url === "string"
59+
);
60+
});
61+
};
62+
2863
const ContributorsPage = () => {
2964
const [contributors, setContributors] = useState<Contributor[]>([]);
3065
const [loading, setLoading] = useState<boolean>(true);
31-
const [error, setError] = useState<string | null>(null);
32-
const [copiedId, setCopiedId] = useState<number | null>(null);
66+
const [error, setError] = useState<FetchError | null>(null);
3367

3468
const handleCopy = async (contributor: Contributor) => {
3569
await navigator.clipboard.writeText(contributor.html_url);
@@ -43,12 +77,60 @@ const handleCopy = async (contributor: Contributor) => {
4377
useEffect(() => {
4478
const fetchContributors = async () => {
4579
try {
80+
setLoading(true);
81+
setError(null);
82+
4683
const response = await axios.get(GITHUB_REPO_CONTRIBUTORS_URL, {
4784
withCredentials: false,
85+
timeout: 10000,
4886
});
87+
88+
// ✅ Validate response structure matches Contributor[]
89+
if (!isContributorArray(response.data)) {
90+
throw new ContributorsError(
91+
"Invalid API response structure. Expected array of contributors.",
92+
false
93+
);
94+
}
95+
4996
setContributors(response.data);
50-
} catch {
51-
setError("Failed to fetch contributors. Please try again later.");
97+
} catch (err) {
98+
const fetchError: FetchError = {
99+
message: "Failed to fetch contributors. Please try again later.",
100+
isRateLimited: false,
101+
};
102+
103+
// Handle ContributorsError instances
104+
if (err instanceof ContributorsError) {
105+
fetchError.message = err.message;
106+
fetchError.isRateLimited = err.isRateLimited;
107+
fetchError.statusCode = err.statusCode;
108+
} else if (axios.isAxiosError(err)) {
109+
// Handle Axios errors
110+
if (err.response?.status === 403) {
111+
fetchError.message =
112+
"GitHub API rate limit exceeded. Try again later.";
113+
fetchError.isRateLimited = true;
114+
fetchError.statusCode = 403;
115+
} else if (err.response?.status === 404) {
116+
fetchError.message = "Repository not found.";
117+
fetchError.statusCode = 404;
118+
} else if (err.code === "ECONNABORTED") {
119+
fetchError.message = "Request timeout. Server took too long to respond.";
120+
fetchError.statusCode = 408;
121+
} else if (err.response?.status) {
122+
fetchError.message = `HTTP ${err.response.status}: Failed to fetch contributors`;
123+
fetchError.statusCode = err.response.status;
124+
} else if (err.message) {
125+
fetchError.message = err.message;
126+
}
127+
} else if (err instanceof Error) {
128+
fetchError.message = err.message || fetchError.message;
129+
}
130+
131+
setError(fetchError);
132+
console.error("Contributors fetch error:", fetchError);
133+
setContributors([]);
52134
} finally {
53135
setLoading(false);
54136
}
@@ -67,8 +149,27 @@ const handleCopy = async (contributor: Contributor) => {
67149

68150
if (error) {
69151
return (
70-
<Box sx={{ mt: 4 }}>
71-
<Alert severity="error">{error}</Alert>
152+
<Box sx={{ mt: 4, mx: 2 }}>
153+
<Alert severity="error" sx={{ mb: 2 }}>
154+
<Typography variant="body2" sx={{ fontWeight: "bold" }}>
155+
⚠️ {error.message}
156+
</Typography>
157+
{error.isRateLimited && (
158+
<Typography variant="caption" sx={{ display: "block", mt: 1 }}>
159+
You've hit GitHub's API rate limit. The limit resets in 1 hour.
160+
</Typography>
161+
)}
162+
{error.statusCode === 404 && (
163+
<Typography variant="caption" sx={{ display: "block", mt: 1 }}>
164+
Please verify the repository exists and is accessible.
165+
</Typography>
166+
)}
167+
{error.statusCode === 408 && (
168+
<Typography variant="caption" sx={{ display: "block", mt: 1 }}>
169+
The server took too long to respond. Please try again.
170+
</Typography>
171+
)}
172+
</Alert>
72173
</Box>
73174
);
74175
}

0 commit comments

Comments
 (0)