From dbc8d985fe7e7b253f150bfb8cae4d8a2b269361 Mon Sep 17 00:00:00 2001 From: admin Date: Tue, 26 May 2026 10:05:09 +0530 Subject: [PATCH] fix: validate Contributors API response schema --- src/pages/Contributors/Contributors.tsx | 112 ++++++++++++++++++++++-- 1 file changed, 107 insertions(+), 5 deletions(-) diff --git a/src/pages/Contributors/Contributors.tsx b/src/pages/Contributors/Contributors.tsx index d4fee52c..b1b0f1ae 100644 --- a/src/pages/Contributors/Contributors.tsx +++ b/src/pages/Contributors/Contributors.tsx @@ -24,21 +24,104 @@ interface Contributor { html_url: string; } +interface FetchError { + message: string; + isRateLimited: boolean; + statusCode?: number; +} + +// Custom error class for Contributors fetch errors +class ContributorsError extends Error { + constructor( + message: string, + public isRateLimited = false, + public statusCode?: number + ) { + super(message); + this.name = "ContributorsError"; + } +} + +// Type guard to validate if data is Contributor[] +const isContributorArray = (data: unknown): data is Contributor[] => { + if (!Array.isArray(data)) return false; + return data.every((item) => { + if (typeof item !== "object" || item === null) return false; + + // Validate all required fields with correct types + return ( + typeof item.id === "number" && + typeof item.login === "string" && + typeof item.avatar_url === "string" && + typeof item.contributions === "number" && + typeof item.html_url === "string" + ); + }); +}; + const ContributorsPage = () => { const [contributors, setContributors] = useState([]); const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); + const [error, setError] = useState(null); // Fetch contributors from GitHub API useEffect(() => { const fetchContributors = async () => { try { + setLoading(true); + setError(null); + const response = await axios.get(GITHUB_REPO_CONTRIBUTORS_URL, { withCredentials: false, + timeout: 10000, }); + + // ✅ Validate response structure matches Contributor[] + if (!isContributorArray(response.data)) { + throw new ContributorsError( + "Invalid API response structure. Expected array of contributors.", + false + ); + } + setContributors(response.data); - } catch { - setError("Failed to fetch contributors. Please try again later."); + } catch (err) { + const fetchError: FetchError = { + message: "Failed to fetch contributors. Please try again later.", + isRateLimited: false, + }; + + // Handle ContributorsError instances + if (err instanceof ContributorsError) { + fetchError.message = err.message; + fetchError.isRateLimited = err.isRateLimited; + fetchError.statusCode = err.statusCode; + } else if (axios.isAxiosError(err)) { + // Handle Axios errors + if (err.response?.status === 403) { + fetchError.message = + "GitHub API rate limit exceeded. Try again later."; + fetchError.isRateLimited = true; + fetchError.statusCode = 403; + } else if (err.response?.status === 404) { + fetchError.message = "Repository not found."; + fetchError.statusCode = 404; + } else if (err.code === "ECONNABORTED") { + fetchError.message = "Request timeout. Server took too long to respond."; + fetchError.statusCode = 408; + } else if (err.response?.status) { + fetchError.message = `HTTP ${err.response.status}: Failed to fetch contributors`; + fetchError.statusCode = err.response.status; + } else if (err.message) { + fetchError.message = err.message; + } + } else if (err instanceof Error) { + fetchError.message = err.message || fetchError.message; + } + + setError(fetchError); + console.error("Contributors fetch error:", fetchError); + setContributors([]); } finally { setLoading(false); } @@ -57,8 +140,27 @@ const ContributorsPage = () => { if (error) { return ( - - {error} + + + + ⚠️ {error.message} + + {error.isRateLimited && ( + + You've hit GitHub's API rate limit. The limit resets in 1 hour. + + )} + {error.statusCode === 404 && ( + + Please verify the repository exists and is accessible. + + )} + {error.statusCode === 408 && ( + + The server took too long to respond. Please try again. + + )} + ); }