Skip to content

Commit dbc8d98

Browse files
committed
fix: validate Contributors API response schema
1 parent 6c6bc3e commit dbc8d98

1 file changed

Lines changed: 107 additions & 5 deletions

File tree

src/pages/Contributors/Contributors.tsx

Lines changed: 107 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,21 +24,104 @@ interface Contributor {
2424
html_url: string;
2525
}
2626

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

3267
// Fetch contributors from GitHub API
3368
useEffect(() => {
3469
const fetchContributors = async () => {
3570
try {
71+
setLoading(true);
72+
setError(null);
73+
3674
const response = await axios.get(GITHUB_REPO_CONTRIBUTORS_URL, {
3775
withCredentials: false,
76+
timeout: 10000,
3877
});
78+
79+
// ✅ Validate response structure matches Contributor[]
80+
if (!isContributorArray(response.data)) {
81+
throw new ContributorsError(
82+
"Invalid API response structure. Expected array of contributors.",
83+
false
84+
);
85+
}
86+
3987
setContributors(response.data);
40-
} catch {
41-
setError("Failed to fetch contributors. Please try again later.");
88+
} catch (err) {
89+
const fetchError: FetchError = {
90+
message: "Failed to fetch contributors. Please try again later.",
91+
isRateLimited: false,
92+
};
93+
94+
// Handle ContributorsError instances
95+
if (err instanceof ContributorsError) {
96+
fetchError.message = err.message;
97+
fetchError.isRateLimited = err.isRateLimited;
98+
fetchError.statusCode = err.statusCode;
99+
} else if (axios.isAxiosError(err)) {
100+
// Handle Axios errors
101+
if (err.response?.status === 403) {
102+
fetchError.message =
103+
"GitHub API rate limit exceeded. Try again later.";
104+
fetchError.isRateLimited = true;
105+
fetchError.statusCode = 403;
106+
} else if (err.response?.status === 404) {
107+
fetchError.message = "Repository not found.";
108+
fetchError.statusCode = 404;
109+
} else if (err.code === "ECONNABORTED") {
110+
fetchError.message = "Request timeout. Server took too long to respond.";
111+
fetchError.statusCode = 408;
112+
} else if (err.response?.status) {
113+
fetchError.message = `HTTP ${err.response.status}: Failed to fetch contributors`;
114+
fetchError.statusCode = err.response.status;
115+
} else if (err.message) {
116+
fetchError.message = err.message;
117+
}
118+
} else if (err instanceof Error) {
119+
fetchError.message = err.message || fetchError.message;
120+
}
121+
122+
setError(fetchError);
123+
console.error("Contributors fetch error:", fetchError);
124+
setContributors([]);
42125
} finally {
43126
setLoading(false);
44127
}
@@ -57,8 +140,27 @@ const ContributorsPage = () => {
57140

58141
if (error) {
59142
return (
60-
<Box sx={{ mt: 4 }}>
61-
<Alert severity="error">{error}</Alert>
143+
<Box sx={{ mt: 4, mx: 2 }}>
144+
<Alert severity="error" sx={{ mb: 2 }}>
145+
<Typography variant="body2" sx={{ fontWeight: "bold" }}>
146+
⚠️ {error.message}
147+
</Typography>
148+
{error.isRateLimited && (
149+
<Typography variant="caption" sx={{ display: "block", mt: 1 }}>
150+
You've hit GitHub's API rate limit. The limit resets in 1 hour.
151+
</Typography>
152+
)}
153+
{error.statusCode === 404 && (
154+
<Typography variant="caption" sx={{ display: "block", mt: 1 }}>
155+
Please verify the repository exists and is accessible.
156+
</Typography>
157+
)}
158+
{error.statusCode === 408 && (
159+
<Typography variant="caption" sx={{ display: "block", mt: 1 }}>
160+
The server took too long to respond. Please try again.
161+
</Typography>
162+
)}
163+
</Alert>
62164
</Box>
63165
);
64166
}

0 commit comments

Comments
 (0)