@@ -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+
2863const 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
3468const 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