Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion src/services/codechef.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,14 @@ function getDivision(rating) {
export async function getCodeChefData(handle) {
try {
const safeHandle = encodeURIComponent(handle);
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 8000);

const res = await fetch(
`https://competeapi.vercel.app/user/codechef/${safeHandle}/`
`https://competeapi.vercel.app/user/codechef/${safeHandle}/`,
{ signal: controller.signal }
);
clearTimeout(timeout);
const data = await res.json();

if (!data || !data.username) {
Expand All @@ -31,6 +36,9 @@ export async function getCodeChefData(handle) {
}
};
} catch (err) {
if (err.name === 'AbortError') {
return { success: false, error: 'CodeChef API timeout' };
}
return { success: false, error: err.message };
}
}
12 changes: 10 additions & 2 deletions src/services/codeforces.service.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
export async function getCodeforcesData(handle) {
try {
const safeHandle = encodeURIComponent(handle);
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 8000);

const [infoRes, statusRes] = await Promise.all([
fetch(`https://codeforces.com/api/user.info?handles=${safeHandle}`),
fetch(`https://codeforces.com/api/user.status?handle=${safeHandle}&from=1&count=10000`),
fetch(`https://codeforces.com/api/user.info?handles=${safeHandle}`, { signal: controller.signal }),
fetch(`https://codeforces.com/api/user.status?handle=${safeHandle}&from=1&count=10000`, { signal: controller.signal }),
]);

clearTimeout(timeout);

const infoData = await infoRes.json();
if (infoData.status !== 'OK') {
return { success: false, error: 'User not found' };
Expand Down Expand Up @@ -42,6 +47,9 @@ export async function getCodeforcesData(handle) {
}
};
} catch (err) {
if (err.name === 'AbortError') {
return { success: false, error: 'Codeforces API timeout' };
}
return { success: false, error: err.message };
}
}
62 changes: 38 additions & 24 deletions src/services/github-graphql.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,35 +54,49 @@ async function fetchContributionData(username) {
throw new Error("GITHUB_TOKEN required for contribution data");
}

const response = await fetch(GITHUB_GRAPHQL_URL, {
method: "POST",
headers: getHeaders(),
body: JSON.stringify({
query: CONTRIBUTION_QUERY,
variables: { username },
}),
});

// handles rate limits silently
const rateLimitRemaining = response.headers.get("x-ratelimit-remaining");
if (rateLimitRemaining && parseInt(rateLimitRemaining, 10) < 10) {
// rate limit warning suppressed for production
}
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 8000);

if (!response.ok) {
if (response.status === 403) {
throw new Error("GitHub API rate limit exceeded");
try {
const response = await fetch(GITHUB_GRAPHQL_URL, {
method: "POST",
headers: getHeaders(),
body: JSON.stringify({
query: CONTRIBUTION_QUERY,
variables: { username },
}),
signal: controller.signal,
});

clearTimeout(timeout);

// handles rate limits silently
const rateLimitRemaining = response.headers.get("x-ratelimit-remaining");
if (rateLimitRemaining && parseInt(rateLimitRemaining, 10) < 10) {
// rate limit warning suppressed for production
}
throw new Error(`GitHub GraphQL API error: ${response.status}`);
}

const json = await response.json();
if (!response.ok) {
if (response.status === 403) {
throw new Error("GitHub API rate limit exceeded");
}
throw new Error(`GitHub GraphQL API error: ${response.status}`);
}

if (json.errors) {
throw new Error(json.errors[0]?.message || "GraphQL query failed");
}
const json = await response.json();

return json.data?.user;
if (json.errors) {
throw new Error(json.errors[0]?.message || "GraphQL query failed");
}

return json.data?.user;
} catch (error) {
clearTimeout(timeout);
if (error.name === "AbortError") {
throw new Error("GitHub GraphQL API timeout");
}
throw error;
}
}

/* flatten contribution calendar weeks into a sorted array of days */
Expand Down
19 changes: 14 additions & 5 deletions src/services/github.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,15 @@ async function assertOk(response) {

/* fetch user profile from GitHub API */
async function fetchUserProfile(username) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 8000);

const response = await fetch(`${GITHUB_API_BASE}/users/${username}`, {
headers: getHeaders(),
signal: controller.signal,
});

clearTimeout(timeout);
await assertOk(response);
return response.json();
}
Expand All @@ -84,11 +89,15 @@ async function fetchUserRepos(username) {
const MAX_PAGES = 3;

while (page <= MAX_PAGES) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 8000);

const response = await fetch(
`${GITHUB_API_BASE}/users/${username}/repos?per_page=${perPage}&page=${page}&sort=updated`,
{ headers: getHeaders() }
{ headers: getHeaders(), signal: controller.signal }
);

clearTimeout(timeout);
await assertOk(response);

const data = await response.json();
Expand All @@ -115,13 +124,13 @@ async function fetchAvatarDataUri(avatarUrl) {
return null;
}

// Use GitHub CDN resizing to get 96x96 image (under 5KB)
// Use GitHub CDN to request 96×96 image (keeps response small)
const resizedUrl = `${avatarUrl}&s=96`;
const MAX_SIZE_BYTES = 100 * 1024; // 100KB limit
const TIMEOUT_MS = 4000; // 4 second timeout
const AVATAR_TIMEOUT_MS = 8000;
const MAX_SIZE_BYTES = 100 * 1024; // 100 KB safety limit

const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS);
const timeout = setTimeout(() => controller.abort(), AVATAR_TIMEOUT_MS);

try {
const response = await fetch(resizedUrl, {
Expand Down
8 changes: 8 additions & 0 deletions src/utils/logger.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ async function connectToDatabase() {
return true; // already connected
}

if (connectionAttempted) {
console.warn('⚠️ Skipping MongoDB connection (already attempted — see previous error)');
return false;
}

if (connectingPromise) {
return connectingPromise; // reuse in-flight attempt
}
Expand All @@ -18,6 +23,7 @@ async function connectToDatabase() {

if (!mongoUri) {
console.error('❌ MONGODB_URI is not set');
connectionAttempted = true;
return false;
}

Expand All @@ -30,10 +36,12 @@ async function connectToDatabase() {
}).then(() => {
console.log(`✅ MongoDB connected for logging (db: ${mongoose.connection.db.databaseName})`);
connectingPromise = null;
connectionAttempted = true;
return true;
}).catch((error) => {
console.error('❌ MongoDB connection failed:', error?.message || error);
connectingPromise = null;
connectionAttempted = true;
return false;
});

Expand Down
Loading