feat: Frontend-API Integration — Replace All Mock Data#317
feat: Frontend-API Integration — Replace All Mock Data#317ItachiDevv wants to merge 1 commit intoSolFoundry:mainfrom
Conversation
SolFoundry#185) - Add centralized apiClient with auth headers, retry, TTL cache keyed by URL + auth token (prevents cross-session cache leaks) - setAuthToken() auto-clears cache on login/logout - Migrate useBountyBoard, useTreasuryStats, AgentProfilePage, ContributorDashboard to use shared apiClient (no raw fetch) - ContributorProfilePage: exact username lookup instead of fuzzy search, resets state on route transitions - useTreasuryStats: surfaces error state explicitly instead of silently falling back to zero values - Inline ErrorBoundary in App.tsx, typed error handling throughout - Zero mock data imports remain in production code Wallet: 97VihHW2Br7BKUU16c7RxjiEMHsD4dWisGDT2Y3LyJxF
📝 WalkthroughWalkthroughThis pull request systematically replaces mock data across the frontend with real API integration. It introduces a new Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested labels
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
❌ Multi-LLM Code Review — REQUEST CHANGESAggregated Score: 5.5/10 (median of 3 models) Model Verdicts
Category Scores (Median)
Warning: Bounty Spec Compliance: PARTIALThis submission partially meets the acceptance criteria. Review the issues above for gaps. SummaryGPT-5.4: This PR makes real progress by introducing a reusable API client and replacing several mock-backed flows with live API calls, but it does not fully satisfy the bounty requirements at T2 quality. The biggest gaps are missing React Query/SWR, incomplete loading/error/retry coverage, no tests, and some endpoint/contract mismatches and fallback behavior that can silently mask failures instead of handling them robustly. Issues
Suggestions
Contributor stats: 11 merged bounty PRs, rep score 100 SolFoundry Multi-LLM Review Pipeline v2.0 — GPT-5.4 + Gemini 2.5 Pro + Grok 4 Next StepsPlease address the issues above and push updated commits. The review will re-run automatically. |
There was a problem hiding this comment.
Actionable comments posted: 8
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
frontend/src/hooks/useBountyBoard.ts (1)
152-170:⚠️ Potential issue | 🟡 MinorHot and recommended bounty fetches lack abort cleanup.
The search effect (lines 112-138) properly uses an AbortController, but the hot bounties (lines 152-159) and recommended bounties (lines 162-170) effects don't abort pending requests on unmount or dependency change. This could cause state updates after unmount.
♻️ Suggested pattern for hot bounties
useEffect(() => { + let cancelled = false; (async () => { try { const d = await apiClient<unknown[]>('/api/bounties/hot?limit=6', { retries: 0, cacheTtl: 60_000 }); - setHotBounties(d.map(mapApiBounty)); + if (!cancelled) setHotBounties(d.map(mapApiBounty)); } catch { /* API unavailable */ } })(); + return () => { cancelled = true; }; }, []);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/hooks/useBountyBoard.ts` around lines 152 - 170, The hot and recommended effects should cancel in-flight API calls to avoid state updates after unmount: create an AbortController inside each useEffect, pass its signal to the apiClient call (the calls to apiClient in the anonymous async IIFEs that currently setHotBounties and setRecommendedBounties), and return a cleanup function that calls controller.abort(); for the recommended effect, keep the dependency on filters.skills but still abort on change. Ensure you catch abort errors appropriately (do not call setHotBounties/setRecommendedBounties if aborted).frontend/src/App.tsx (1)
57-85: 🧹 Nitpick | 🔵 TrivialErrorBoundary placement only catches route component errors.
The
ErrorBoundaryis insideSiteLayout, so errors inSiteLayoutitself (header, navigation, footer) won't be caught and will crash the entire app. Consider wrappingSiteLayoutwith the boundary, or adding a second boundary at a higher level.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/App.tsx` around lines 57 - 85, The ErrorBoundary is currently inside SiteLayout and thus cannot catch errors thrown by SiteLayout (header/nav/footer); update App.tsx to wrap the SiteLayout component with ErrorBoundary (or add a second ErrorBoundary above SiteLayout) so that errors in SiteLayout and its children are caught; look for the ErrorBoundary usage and the SiteLayout component in the Routes/App root and move or add the boundary to enclose SiteLayout (and its header/footer) rather than placing the boundary only around route elements.frontend/src/hooks/useLeaderboard.ts (1)
86-100: 🧹 Nitpick | 🔵 TrivialInconsistent API client usage — this hook still uses raw
fetchfor backend API.Unlike
useTreasuryStatsanduseBountyBoard, this hook uses rawfetch('/api/leaderboard')(line 89) instead of the centralizedapiClient. This bypasses auth header injection, retry logic, and caching. Consider migrating toapiClientfor consistency.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/hooks/useLeaderboard.ts` around lines 86 - 100, The hook useLeaderboard currently calls fetch('/api/leaderboard?range=...') directly which bypasses the shared apiClient used in useTreasuryStats and useBountyBoard; replace the raw fetch with the centralized apiClient call (e.g., apiClient.get or apiClient.request to the same endpoint with the range param), import apiClient if missing, and adapt the response handling to the client’s return shape (parse/inspect response data the same way before calling setContributors and setLoading) so auth headers, retries and caching are used consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@frontend/src/components/ContributorDashboard.tsx`:
- Line 66: The safe function currently swallows all errors and returns null
which hides API failures; change safe<T>(...) to return a discriminated Result
type (e.g., { ok: true; data: T } | { ok: false; error: unknown }) instead of T
| null, call apiClient<T>(...) inside try and return { ok: true, data }, and in
catch log the error (include endpoint and params) and return { ok: false, error
} so callers can distinguish empty data from network/authorization errors;
update code paths that call safe to handle the Result shape.
- Line 69: The bounties request currently sends claimed_by: '' when userId is
undefined; update the Promise.all call in ContributorDashboard (the const [bRaw,
nRaw, lRaw] = await Promise.all(...) line) to avoid sending an empty claimed_by:
either build the requests array conditionally (only push safe('/api/bounties', {
claimed_by: userId, limit: 10 }) when userId is truthy) and call Promise.all on
that array, or replace the bounties entry with a resolved placeholder (e.g.,
Promise.resolve(null)) when userId is falsy so bRaw becomes null/empty instead
of calling the backend with an empty string; ensure you still await
notifications and leaderboard via safe(...) and adjust subsequent code that
reads bRaw accordingly.
In `@frontend/src/pages/AgentProfilePage.tsx`:
- Around line 11-22: mapAgent currently uses unsafe casts for role and status
((r.role as AgentProfileType['role']) and (r.status as
AgentProfileType['status']) ) which let unexpected API values through; validate
values against explicit whitelists before assigning and fall back to defaults.
Add const VALID_ROLES and VALID_STATUSES arrays, then compute role and status by
checking membership (e.g., VALID_ROLES.includes(r.role as any) ? r.role as
AgentProfileType['role'] : 'developer') and similarly for status, and use those
validated variables in the returned object instead of the direct casts.
- Line 35: The apiClient call in AgentProfilePage uses agentId directly in the
path (const data = await apiClient<Record<string,
unknown>>(`/api/agents/${agentId}`, ...)), which can break when agentId contains
special characters; update the call to URL-encode agentId (use
encodeURIComponent(agentId)) before interpolating into the `/api/agents/...`
path—mirror the approach used in ContributorProfilePage so the request always
uses an encoded agentId.
In `@frontend/src/pages/ContributorProfilePage.tsx`:
- Around line 19-24: The catch block in ContributorProfilePage.tsx silently
replaces failed apiClient(...) calls with a minimal profile, so update the error
handling to surface failures: catch the error, log it (include the thrown
error), and set an error/partial flag in state (e.g., setError or
setPartialData) instead of only calling setP({ username }); ensure you still
stop the loading spinner via setL(false) and use the new error/partial state in
the UI to show an error message or a "partial data" indicator; reference the
existing symbols apiClient, username, setP, setL, and ok when making these
changes.
In `@frontend/src/services/apiClient.ts`:
- Line 11: The cache key helper function ck currently uses token.slice(-8) which
risks collisions; update ck to derive the key from a hash of the full token
instead of the last 8 characters. Locate the ck function and replace the
suffix-based logic (token.slice(-8)) with a secure, deterministic hash of the
entire token (e.g., SHA-256 or an existing project hash util) and use that hash
in the returned cache key; ensure the hashing call is synchronous/awaited based
on the chosen API and preserve the existing `${...}:${url}` format.
- Line 19: The headers object h is always initialized with 'Content-Type':
'application/json' which applies to GETs; update the logic around h (the
Record<string,string> created from hx) to only add 'Content-Type' when there is
a request body (or when method is not GET/HEAD) — e.g., create h from hx first,
then if body !== undefined && body !== null (or method !== 'GET' && method !==
'HEAD') set h['Content-Type'] = 'application/json'; keep references to h and hx
as-is so the change is localized to the header initialization in apiClient.ts.
- Around line 22-29: The fetch in the retry loop inside apiClient.ts (inside the
for loop that uses retries) has no timeout and can hang; update the fetch call
in that loop to include a request timeout by passing a signal (use
AbortSignal.timeout(ms) if available or create an AbortController and setTimeout
to call controller.abort()), ensure the signal is added to the fetch options
alongside method: m, headers: h and body, and clear any timers if using
AbortController so retries and error handling in the surrounding catch
(isApiError checks and last assignment) behave correctly.
---
Outside diff comments:
In `@frontend/src/App.tsx`:
- Around line 57-85: The ErrorBoundary is currently inside SiteLayout and thus
cannot catch errors thrown by SiteLayout (header/nav/footer); update App.tsx to
wrap the SiteLayout component with ErrorBoundary (or add a second ErrorBoundary
above SiteLayout) so that errors in SiteLayout and its children are caught; look
for the ErrorBoundary usage and the SiteLayout component in the Routes/App root
and move or add the boundary to enclose SiteLayout (and its header/footer)
rather than placing the boundary only around route elements.
In `@frontend/src/hooks/useBountyBoard.ts`:
- Around line 152-170: The hot and recommended effects should cancel in-flight
API calls to avoid state updates after unmount: create an AbortController inside
each useEffect, pass its signal to the apiClient call (the calls to apiClient in
the anonymous async IIFEs that currently setHotBounties and
setRecommendedBounties), and return a cleanup function that calls
controller.abort(); for the recommended effect, keep the dependency on
filters.skills but still abort on change. Ensure you catch abort errors
appropriately (do not call setHotBounties/setRecommendedBounties if aborted).
In `@frontend/src/hooks/useLeaderboard.ts`:
- Around line 86-100: The hook useLeaderboard currently calls
fetch('/api/leaderboard?range=...') directly which bypasses the shared apiClient
used in useTreasuryStats and useBountyBoard; replace the raw fetch with the
centralized apiClient call (e.g., apiClient.get or apiClient.request to the same
endpoint with the range param), import apiClient if missing, and adapt the
response handling to the client’s return shape (parse/inspect response data the
same way before calling setContributors and setLoading) so auth headers, retries
and caching are used consistently.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 9e284673-fe53-4ac7-babd-95dbfe89381b
📒 Files selected for processing (8)
frontend/src/App.tsxfrontend/src/components/ContributorDashboard.tsxfrontend/src/hooks/useBountyBoard.tsfrontend/src/hooks/useLeaderboard.tsfrontend/src/hooks/useTreasuryStats.tsfrontend/src/pages/AgentProfilePage.tsxfrontend/src/pages/ContributorProfilePage.tsxfrontend/src/services/apiClient.ts
|
|
||
| interface DashboardData { stats: DashboardStats; bounties: Bounty[]; activities: Activity[]; notifications: Notification[]; earnings: EarningsData[]; linkedAccounts: { type: string; username: string; connected: boolean }[]; } | ||
| const ES: DashboardStats = { totalEarned: 0, activeBounties: 0, pendingPayouts: 0, reputationRank: 0, totalContributors: 0 }; | ||
| async function safe<T>(ep: string, p?: Record<string, string | number | boolean | undefined>): Promise<T | null> { try { return await apiClient<T>(ep, { params: p, retries: 0 }); } catch { return null; } } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Error swallowing prevents debugging and user feedback.
The safe() wrapper catches all errors and returns null, making it impossible to distinguish between "API returned empty data" vs "API is down" vs "user is unauthorized." Consider returning a result type or at least logging errors.
♻️ Alternative with error visibility
type Result<T> = { ok: true; data: T } | { ok: false; error: unknown };
async function safe<T>(ep: string, p?: Record<string, string | number | boolean | undefined>): Promise<Result<T>> {
try {
return { ok: true, data: await apiClient<T>(ep, { params: p, retries: 0 }) };
} catch (error) {
console.warn(`[Dashboard] ${ep} failed:`, error);
return { ok: false, error };
}
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/components/ContributorDashboard.tsx` at line 66, The safe
function currently swallows all errors and returns null which hides API
failures; change safe<T>(...) to return a discriminated Result type (e.g., { ok:
true; data: T } | { ok: false; error: unknown }) instead of T | null, call
apiClient<T>(...) inside try and return { ok: true, data }, and in catch log the
error (include endpoint and params) and return { ok: false, error } so callers
can distinguish empty data from network/authorization errors; update code paths
that call safe to handle the Result shape.
| linkedAccounts: MOCK_LINKED_ACCOUNTS, | ||
| }; | ||
| const d: DashboardData = { stats: { ...ES }, bounties: [], activities: [], notifications: [], earnings: [], linkedAccounts: [] }; | ||
| const [bRaw, nRaw, lRaw] = await Promise.all([safe<{ items?: unknown[] }>('/api/bounties', { claimed_by: userId ?? '', limit: 10 }), safe<{ items?: unknown[] }>('/api/notifications', { limit: 10 }), safe<unknown[]>('/api/leaderboard', { range: 'all', limit: 50 })]); |
There was a problem hiding this comment.
Empty claimed_by param sent when userId is undefined.
When userId is undefined, the code sends claimed_by: '' which may cause unexpected backend behavior. Consider skipping the bounties call entirely for anonymous users, or verify the backend handles empty claimed_by gracefully.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/components/ContributorDashboard.tsx` at line 69, The bounties
request currently sends claimed_by: '' when userId is undefined; update the
Promise.all call in ContributorDashboard (the const [bRaw, nRaw, lRaw] = await
Promise.all(...) line) to avoid sending an empty claimed_by: either build the
requests array conditionally (only push safe('/api/bounties', { claimed_by:
userId, limit: 10 }) when userId is truthy) and call Promise.all on that array,
or replace the bounties entry with a resolved placeholder (e.g.,
Promise.resolve(null)) when userId is falsy so bRaw becomes null/empty instead
of calling the backend with an empty string; ensure you still await
notifications and leaderboard via safe(...) and adjust subsequent code that
reads bRaw accordingly.
| function mapAgent(r: Record<string, unknown>): AgentProfileType { | ||
| const cb = Array.isArray(r.completed_bounties) ? r.completed_bounties as Record<string, unknown>[] : []; | ||
| return { | ||
| id: String(r.id ?? ''), name: String(r.name ?? ''), avatar: String(r.avatar ?? r.avatar_url ?? ''), | ||
| role: (r.role as AgentProfileType['role']) ?? 'developer', status: (r.status as AgentProfileType['status']) ?? 'offline', | ||
| bio: String(r.bio ?? r.description ?? ''), skills: (r.skills ?? []) as string[], languages: (r.languages ?? []) as string[], | ||
| bountiesCompleted: Number(r.bounties_completed ?? 0), successRate: Number(r.success_rate ?? 0), | ||
| avgReviewScore: Number(r.avg_review_score ?? 0), totalEarned: Number(r.total_earned ?? 0), | ||
| completedBounties: cb.map(b => ({ id: String(b.id ?? ''), title: String(b.title ?? ''), completedAt: String(b.completed_at ?? ''), score: Number(b.score ?? 0), reward: Number(b.reward ?? 0), currency: '$FNDRY' })), | ||
| joinedAt: String(r.joined_at ?? r.created_at ?? ''), | ||
| }; | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Unsafe type assertions for enum-like fields.
The casts (r.role as AgentProfileType['role']) ?? 'developer' and (r.status as AgentProfileType['status']) ?? 'offline' don't validate the actual values. If the API returns an unexpected value (e.g., role: "admin"), it passes through unchecked and could cause downstream rendering issues.
♻️ Safer pattern with validation
const VALID_ROLES = ['developer', 'reviewer', 'manager'] as const;
const VALID_STATUSES = ['online', 'offline', 'busy'] as const;
const role = VALID_ROLES.includes(r.role as any) ? r.role as AgentProfileType['role'] : 'developer';
const status = VALID_STATUSES.includes(r.status as any) ? r.status as AgentProfileType['status'] : 'offline';🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/pages/AgentProfilePage.tsx` around lines 11 - 22, mapAgent
currently uses unsafe casts for role and status ((r.role as
AgentProfileType['role']) and (r.status as AgentProfileType['status']) ) which
let unexpected API values through; validate values against explicit whitelists
before assigning and fall back to defaults. Add const VALID_ROLES and
VALID_STATUSES arrays, then compute role and status by checking membership
(e.g., VALID_ROLES.includes(r.role as any) ? r.role as AgentProfileType['role']
: 'developer') and similarly for status, and use those validated variables in
the returned object instead of the direct casts.
| setLoading(true); setNotFound(false); setAgent(null); | ||
| (async () => { | ||
| try { | ||
| const data = await apiClient<Record<string, unknown>>(`/api/agents/${agentId}`, { retries: 1 }); |
There was a problem hiding this comment.
Missing URL encoding for agentId parameter.
Unlike ContributorProfilePage which uses encodeURIComponent(username), this page passes agentId directly into the URL path. If agentId contains special characters (e.g., /, ?), this could cause routing issues or unintended API calls.
- const data = await apiClient<Record<string, unknown>>(`/api/agents/${agentId}`, { retries: 1 });
+ const data = await apiClient<Record<string, unknown>>(`/api/agents/${encodeURIComponent(agentId)}`, { retries: 1 });📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const data = await apiClient<Record<string, unknown>>(`/api/agents/${agentId}`, { retries: 1 }); | |
| const data = await apiClient<Record<string, unknown>>(`/api/agents/${encodeURIComponent(agentId)}`, { retries: 1 }); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/pages/AgentProfilePage.tsx` at line 35, The apiClient call in
AgentProfilePage uses agentId directly in the path (const data = await
apiClient<Record<string, unknown>>(`/api/agents/${agentId}`, ...)), which can
break when agentId contains special characters; update the call to URL-encode
agentId (use encodeURIComponent(agentId)) before interpolating into the
`/api/agents/...` path—mirror the approach used in ContributorProfilePage so the
request always uses an encoded agentId.
| (async () => { | ||
| try { | ||
| const data = await apiClient<ContributorData>(`/api/contributors/${encodeURIComponent(username)}`, { retries: 1 }); | ||
| if (ok) setP(data); | ||
| } catch { if (ok) setP({ username }); } | ||
| finally { if (ok) setL(false); } |
There was a problem hiding this comment.
Silent error handling — user has no indication of API failure.
When the API call fails (line 23), the catch block silently falls back to a minimal profile with only the username. The user sees a seemingly valid profile with zeroed stats rather than an error state. Consider surfacing the error or showing a "partial data" indicator.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/pages/ContributorProfilePage.tsx` around lines 19 - 24, The
catch block in ContributorProfilePage.tsx silently replaces failed
apiClient(...) calls with a minimal profile, so update the error handling to
surface failures: catch the error, log it (include the thrown error), and set an
error/partial flag in state (e.g., setError or setPartialData) instead of only
calling setP({ username }); ensure you still stop the loading spinner via
setL(false) and use the new error/partial state in the UI to show an error
message or a "partial data" indicator; reference the existing symbols apiClient,
username, setP, setL, and ok when making these changes.
| const cache = new Map<string, { d: unknown; e: number }>(); | ||
| export function clearApiCache(): void { cache.clear(); } | ||
| export function isApiError(e: unknown): e is ApiError { return typeof e === 'object' && e !== null && 'status' in e && 'message' in e; } | ||
| function ck(url: string): string { return token ? `${token.slice(-8)}:${url}` : url; } |
There was a problem hiding this comment.
Cache key collision risk with short token suffix.
Using only the last 8 characters of the token (token.slice(-8)) for cache key differentiation could cause cross-user cache collisions if two different tokens happen to share the same suffix. Consider using a hash of the full token instead.
-function ck(url: string): string { return token ? `${token.slice(-8)}:${url}` : url; }
+function ck(url: string): string { return token ? `${btoa(token).slice(0,16)}:${url}` : url; }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function ck(url: string): string { return token ? `${token.slice(-8)}:${url}` : url; } | |
| function ck(url: string): string { return token ? `${btoa(token).slice(0,16)}:${url}` : url; } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/services/apiClient.ts` at line 11, The cache key helper function
ck currently uses token.slice(-8) which risks collisions; update ck to derive
the key from a hash of the full token instead of the last 8 characters. Locate
the ck function and replace the suffix-based logic (token.slice(-8)) with a
secure, deterministic hash of the entire token (e.g., SHA-256 or an existing
project hash util) and use that hash in the returned cache key; ensure the
hashing call is synchronous/awaited based on the chosen API and preserve the
existing `${...}:${url}` format.
| const m = (r.method ?? (body ? 'POST' : 'GET')).toUpperCase(); | ||
| const key = ck(url); | ||
| if (m === 'GET' && cacheTtl > 0) { const c = cache.get(key); if (c && c.e > Date.now()) return c.d as T; } | ||
| const h: Record<string, string> = { 'Content-Type': 'application/json', ...(hx as Record<string, string>) }; |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Content-Type header set on all requests including GET.
Setting Content-Type: application/json on GET requests is unconventional and some servers may reject it. Consider only setting this header when there's a body.
♻️ Suggested fix
- const h: Record<string, string> = { 'Content-Type': 'application/json', ...(hx as Record<string, string>) };
+ const h: Record<string, string> = { ...(hx as Record<string, string>) };
+ if (body) h['Content-Type'] = 'application/json';🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/services/apiClient.ts` at line 19, The headers object h is
always initialized with 'Content-Type': 'application/json' which applies to
GETs; update the logic around h (the Record<string,string> created from hx) to
only add 'Content-Type' when there is a request body (or when method is not
GET/HEAD) — e.g., create h from hx first, then if body !== undefined && body !==
null (or method !== 'GET' && method !== 'HEAD') set h['Content-Type'] =
'application/json'; keep references to h and hx as-is so the change is localized
to the header initialization in apiClient.ts.
| for (let i = 0; i <= retries; i++) { | ||
| try { | ||
| const res = await fetch(url, { ...r, method: m, headers: h, body: body ? JSON.stringify(body) : undefined }); | ||
| if (!res.ok) { let b: Record<string, string> = {}; try { b = await res.json(); } catch { /* */ } const err: ApiError = { status: res.status, message: b.message ?? b.detail ?? res.statusText, code: b.code ?? `HTTP_${res.status}` }; if (res.status < 500 && res.status !== 429) throw err; last = err; } | ||
| else { const d = (await res.json()) as T; if (m === 'GET' && cacheTtl > 0) cache.set(key, { d, e: Date.now() + cacheTtl }); return d; } | ||
| } catch (e: unknown) { if (isApiError(e) && e.status > 0 && e.status < 500 && e.status !== 429) throw e; last = isApiError(e) ? e : { status: 0, message: e instanceof Error ? e.message : 'Network error', code: 'NETWORK_ERROR' }; } | ||
| if (i < retries) await new Promise(w => setTimeout(w, 300 * 2 ** i)); | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
🧩 Analysis chain
🌐 Web query:
Does fetch API support AbortSignal.timeout in modern browsers?
💡 Result:
Yes—Fetch supports AbortSignal.timeout() in modern browsers, because fetch() accepts an AbortSignal via { signal }, and AbortSignal.timeout(ms) creates a signal that auto-aborts after the given time. [2][3]
Current baseline support (desktop):
- Chrome / Edge: 124+ [1]
- Firefox: 100+ [1]
- Safari (macOS & iOS): 16.0+ [1]
Older browser versions won’t support AbortSignal.timeout(), even if they support fetch() and AbortController. [1][2]
Add request timeout to prevent indefinite hangs.
The fetch call at lines 24-25 has no timeout configured. Each request can hang indefinitely while waiting for a response, even with the exponential backoff retry logic (line 28). Use AbortSignal.timeout(ms) to auto-abort requests after a specified duration:
const res = await fetch(url, {
...r,
method: m,
headers: h,
body: body ? JSON.stringify(body) : undefined,
signal: AbortSignal.timeout(5000) // 5 second timeout example
});Modern browsers (Chrome 124+, Firefox 100+, Safari 16.0+) support AbortSignal.timeout(). For older browser support, use AbortController with setTimeout instead.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/services/apiClient.ts` around lines 22 - 29, The fetch in the
retry loop inside apiClient.ts (inside the for loop that uses retries) has no
timeout and can hang; update the fetch call in that loop to include a request
timeout by passing a signal (use AbortSignal.timeout(ms) if available or create
an AbortController and setTimeout to call controller.abort()), ensure the signal
is added to the fetch options alongside method: m, headers: h and body, and
clear any timers if using AbortController so retries and error handling in the
surrounding catch (isApiError checks and last assignment) behave correctly.
Description
Replaces all mock data imports with real backend API calls across the frontend. Adds centralized API client with auth-scoped caching, retry with exponential backoff, and proper error surfacing.
Closes #185
API Client (frontend/src/services/apiClient.ts)
Hooks Migrated (all use shared apiClient)
Pages Connected
Quality
Solana Wallet for Payout
Wallet:
97VihHW2Br7BKUU16c7RxjiEMHsD4dWisGDT2Y3LyJxFChecklist