Skip to content

Commit 628af17

Browse files
authored
fix(api): disable caching for refresh=true stats requests (JhaSourav07#1981)
## Description This PR fixes cache behavior for the `/api/stats` endpoint when the `refresh=true` query parameter is used. Previously, requests using `refresh=true` correctly bypassed the internal GitHub cache, but the response itself still returned cacheable headers. This allowed downstream caches (CDNs, proxies, or intermediary layers) to potentially store and serve stale responses even after an explicit refresh request. ### Changes Made * Added refresh-aware cache handling for the stats endpoint. * Return non-cacheable headers when `refresh=true` is present: * `Cache-Control: no-store, no-cache, must-revalidate` * `Pragma: no-cache` * `Expires: 0` * Preserved the existing caching strategy for normal requests. * Added regression tests covering: * Refresh requests returning non-cacheable headers. * Refresh requests still returning valid stats data. * Normal requests retaining existing cache behavior. * Refactored header handling using a concrete `Headers` instance to satisfy TypeScript type requirements and ensure production builds succeed. Fixes JhaSourav07#1966 --- ## Pillar 🐛 Bug Fix --- ## Checklist * [x] Linked this PR to an assigned issue * [x] Kept changes scoped to the issue requirements * [x] Added/updated tests where applicable * [x] Verified formatting locally * [x] Verified linting locally * [x] Verified type checking locally * [x] Verified tests locally * [x] Verified production build locally * [x] Reviewed the implementation before submission --- ## Validation Successfully verified locally using: ```bash npm run format:check npm run lint npm run typecheck npm run test npm run build ``` ### Results * Format check: Passed * Lint: Passed (warnings only, no errors) * TypeScript type check: Passed * Test suite: Passed (1199/1199 tests) * Production build: Passed ### Impact This change ensures that users explicitly requesting fresh statistics receive responses that cannot be cached by intermediate layers while preserving the existing caching benefits and performance optimizations for normal requests.
2 parents dcc2c81 + d2e0315 commit 628af17

2 files changed

Lines changed: 41 additions & 6 deletions

File tree

app/api/stats/route.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,36 @@ describe('GET /api/stats', () => {
103103
expect(typeof body.currentStreak).toBe('number');
104104
});
105105

106+
it('returns non-cacheable headers when refresh=true', async () => {
107+
const response = await GET(makeRequest({ user: 'testuser', refresh: 'true' }));
108+
109+
expect(response.status).toBe(200);
110+
expect(response.headers.get('Cache-Control')).toBe('no-store, no-cache, must-revalidate');
111+
expect(response.headers.get('Pragma')).toBe('no-cache');
112+
expect(response.headers.get('Expires')).toBe('0');
113+
});
114+
115+
it('still returns valid stats data when refresh=true', async () => {
116+
const response = await GET(makeRequest({ user: 'testuser', refresh: 'true' }));
117+
const body = await response.json();
118+
119+
expect(response.status).toBe(200);
120+
expect(body.totalContributions).toBe(10);
121+
expect(typeof body.longestStreak).toBe('number');
122+
expect(typeof body.currentStreak).toBe('number');
123+
});
124+
125+
it('keeps the existing cache headers for normal requests', async () => {
126+
const response = await GET(makeRequest({ user: 'testuser' }));
127+
128+
expect(response.status).toBe(200);
129+
expect(response.headers.get('Cache-Control')).toBe(
130+
'public, s-maxage=3600, stale-while-revalidate=86400'
131+
);
132+
expect(response.headers.get('Pragma')).toBeNull();
133+
expect(response.headers.get('Expires')).toBeNull();
134+
});
135+
106136
it('passes bypassCache=true to GitHub when refresh=true', async () => {
107137
await GET(makeRequest({ user: 'testuser', refresh: 'true' }));
108138
expect(fetchGitHubContributions).toHaveBeenCalledWith('testuser', { bypassCache: true });

app/api/stats/route.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,19 +50,24 @@ export async function GET(request: Request) {
5050
const userData = await fetchGitHubContributions(user, { bypassCache: refresh });
5151
const calendar = userData.calendar;
5252
const stats = calculateStreak(calendar, timezone);
53+
const headers = new Headers({
54+
// Cache until next UTC midnight; clients can bust with ?refresh=true
55+
'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=86400',
56+
});
57+
58+
if (refresh) {
59+
headers.set('Cache-Control', 'no-store, no-cache, must-revalidate');
60+
headers.set('Pragma', 'no-cache');
61+
headers.set('Expires', '0');
62+
}
5363

5464
return NextResponse.json(
5565
{
5666
totalContributions: stats.totalContributions,
5767
longestStreak: stats.longestStreak,
5868
currentStreak: stats.currentStreak,
5969
},
60-
{
61-
headers: {
62-
// Cache until next UTC midnight; clients can bust with ?refresh=true
63-
'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=86400',
64-
},
65-
}
70+
{ headers }
6671
);
6772
} catch (error: unknown) {
6873
const message = error instanceof Error ? error.message : 'Unknown error';

0 commit comments

Comments
 (0)