Skip to content

Commit 5dfc014

Browse files
authored
merge: fix: update cache control headers to s-maxage=1 to prevent stale data… (#7821)
## Description Fixes #7803 The SVG endpoints (such as `/api/streak`, `/api/stats`, etc.) were previously setting a `Cache-Control` header of `s-maxage=3600`. Because GitHub caches external images using its own Camo proxy, this high max-age meant that users wouldn't see their updated commit stats for up to an hour after making a contribution. This PR updates the `buildCacheControlHeader` utility and all associated API routes to return `s-maxage=1, stale-while-revalidate=59`. This dramatically reduces the hard caching duration while still allowing CDNs to serve stale content gracefully while revalidating in the background, ensuring near real-time updates for SVG widgets embedded in READMEs. All relevant unit tests testing cache bounds were updated to reflect this new optimal configuration. ## Pillar - [ ] 🎨 Pillar 1 — New Theme Design - [ ] 📐 Pillar 2 — Geometric SVG Improvement - [ ] 🕐 Pillar 3 — Timezone Logic Optimization - [x] 🛠️ Other (Bug fix, refactoring, docs) ## Checklist before requesting a review: - [x] I have read the `CONTRIBUTING.md` file. - [x] I have tested these changes locally (`localhost:3000/api/streak?user=YOUR_USERNAME`). - [x] I have run `npm run format` and `npm run lint` locally and resolved all errors (CI will fail otherwise). - [x] My commits follow the Conventional Commits format (e.g., `feat(themes): ...`, `fix(calculate): ...`). - [ ] I have updated `README.md` if I added a new theme or URL parameter. - [x] I have started the repo. - [x] I have made sure that i have only one commit to merge in this PR. - [x] The SVG output matches the CommitPulse "premium quality" aesthetic standard (no raw elements, smooth animations, correct fonts). - [x] (Recommended) I joined the CommitPulse Discord community for contributor discussions, mentorship, and faster PR support.
2 parents 66de343 + 64dd98a commit 5dfc014

19 files changed

Lines changed: 36 additions & 37 deletions

app/api/compare/route.empty-fallback.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ describe('GET /api/compare - Empty & Missing Input Fallbacks', () => {
6666
const res = await GET(makeRequest('user1=alice&user2=bob'));
6767
expect(res.status).toBe(200);
6868
expect(res.headers.get('ETag')).toBeTruthy();
69-
expect(res.headers.get('Cache-Control')).toBe('public, s-maxage=3600');
69+
expect(res.headers.get('Cache-Control')).toBe('public, s-maxage=1');
7070
});
7171

7272
it('returns 200 when If-None-Match is an empty string instead of crashing', async () => {

app/api/compare/route.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ describe('GET /api/compare', () => {
181181
const res = await GET(makeRequest('user1=alice&user2=bob'));
182182
expect(res.status).toBe(200);
183183
expect(res.headers.get('ETag')).toBeTruthy();
184-
expect(res.headers.get('Cache-Control')).toBe('public, s-maxage=3600');
184+
expect(res.headers.get('Cache-Control')).toBe('public, s-maxage=1');
185185
});
186186

187187
it('returns 304 Not Modified when If-None-Match matches weak ETag', async () => {
@@ -199,7 +199,7 @@ describe('GET /api/compare', () => {
199199
expect(res2.status).toBe(304);
200200
expect(res2.body).toBeNull();
201201
expect(res2.headers.get('ETag')).toBe(etag);
202-
expect(res2.headers.get('Cache-Control')).toBe('public, s-maxage=3600');
202+
expect(res2.headers.get('Cache-Control')).toBe('public, s-maxage=1');
203203
});
204204

205205
it('returns 304 Not Modified when If-None-Match matches strong ETag', async () => {

app/api/compare/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ export async function GET(request: Request) {
103103
const etag = crypto.createHash('sha1').update(jsonPayload).digest('hex');
104104
const weakEtag = `W/"${etag}"`;
105105
const ifNoneMatch = request.headers.get('if-none-match');
106-
const cacheControl = 'public, s-maxage=3600';
106+
const cacheControl = 'public, s-maxage=1';
107107

108108
if (ifNoneMatch) {
109109
const etags = ifNoneMatch.split(',').map((e) => e.trim());

app/api/github/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ export async function GET(request: Request) {
173173

174174
const cacheControl = shouldBypassCache
175175
? 'no-cache, no-store, must-revalidate'
176-
: 's-maxage=3600, stale-while-revalidate=86400';
176+
: 's-maxage=1, stale-while-revalidate=59';
177177

178178
const cacheStatus = shouldBypassCache ? 'MISS' : 'HIT';
179179

app/api/repo-burnout/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ export async function GET(request: Request) {
9797

9898
const cacheControl = shouldBypassCache
9999
? 'no-cache, no-store, must-revalidate'
100-
: 's-maxage=3600, stale-while-revalidate=86400';
100+
: 's-maxage=1, stale-while-revalidate=59';
101101

102102
return NextResponse.json(data, {
103103
status: 200,

app/api/spotlight/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ export async function GET(request: Request) {
104104
return new NextResponse(null, {
105105
status: 304,
106106
headers: {
107-
'Cache-Control': 'public, max-age=60, s-maxage=3600, stale-while-revalidate=60',
107+
'Cache-Control': 'public, max-age=60, s-maxage=1, stale-while-revalidate=59',
108108
ETag: weakEtag,
109109
},
110110
});
@@ -113,7 +113,7 @@ export async function GET(request: Request) {
113113
return new NextResponse(svg, {
114114
headers: {
115115
'Content-Type': 'image/svg+xml; charset=utf-8',
116-
'Cache-Control': 'public, max-age=60, s-maxage=3600, stale-while-revalidate=60',
116+
'Cache-Control': 'public, max-age=60, s-maxage=1, stale-while-revalidate=59',
117117
'Content-Security-Policy': SVG_CSP_HEADER,
118118
ETag: weakEtag,
119119
},

app/api/stats/route.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ describe('GET /api/stats', () => {
141141

142142
expect(response.status).toBe(200);
143143
expect(response.headers.get('Cache-Control')).toBe(
144-
'public, s-maxage=3600, stale-while-revalidate=86400'
144+
'public, s-maxage=1, stale-while-revalidate=59'
145145
);
146146
expect(response.headers.get('Pragma')).toBeNull();
147147
expect(response.headers.get('Expires')).toBeNull();
@@ -168,7 +168,7 @@ describe('GET /api/stats', () => {
168168
});
169169
expect(response.headers.get('X-Refresh-Status')).toBe('Cooldown-Served-Cached');
170170
expect(response.headers.get('Cache-Control')).toBe(
171-
'public, s-maxage=3600, stale-while-revalidate=86400'
171+
'public, s-maxage=1, stale-while-revalidate=59'
172172
);
173173
});
174174

app/api/stats/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ export async function GET(request: Request) {
134134
const stats = calculateStreak(calendar, timezone);
135135
const headers = new Headers({
136136
// Cache until next UTC midnight; clients can bust with ?refresh=true
137-
'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=86400',
137+
'Cache-Control': 'public, s-maxage=1, stale-while-revalidate=59',
138138
});
139139

140140
if (shouldBypassCache) {

app/api/stats/route.validation.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ describe('GET /api/stats additional runtime coverage', () => {
113113
expect(response.status).toBe(200);
114114

115115
expect(response.headers.get('Cache-Control')).toBe(
116-
'public, s-maxage=3600, stale-while-revalidate=86400'
116+
'public, s-maxage=1, stale-while-revalidate=59'
117117
);
118118
expect(response.headers.get('Pragma')).toBeNull();
119119
expect(response.headers.get('Expires')).toBeNull();

app/api/streak/route.test.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -443,15 +443,15 @@ describe('GET /api/streak', () => {
443443
it('caches until UTC midnight by default, using the value from getSecondsUntilUTCMidnight', async () => {
444444
const response = await GET(makeRequest({ user: 'octocat' }));
445445
expect(response.headers.get('Cache-Control')).toBe(
446-
'public, max-age=60, s-maxage=3600, stale-while-revalidate=60'
446+
'public, max-age=60, s-maxage=1, stale-while-revalidate=59'
447447
);
448448
});
449449

450450
it('reflects a different time value when the clock changes', async () => {
451451
vi.mocked(getSecondsUntilUTCMidnight).mockReturnValue(7200);
452452
const response = await GET(makeRequest({ user: 'octocat' }));
453453
expect(response.headers.get('Cache-Control')).toBe(
454-
'public, max-age=60, s-maxage=7200, stale-while-revalidate=60'
454+
'public, max-age=60, s-maxage=1, stale-while-revalidate=59'
455455
);
456456
});
457457

@@ -1013,7 +1013,7 @@ describe('GET /api/streak', () => {
10131013
const response = await GET(makeRequest({ user: 'octocat', tz: 'America/New_York' }));
10141014

10151015
expect(response.headers.get('Cache-Control')).toBe(
1016-
'public, max-age=60, s-maxage=7200, stale-while-revalidate=60'
1016+
'public, max-age=60, s-maxage=1, stale-while-revalidate=59'
10171017
);
10181018
expect(getSecondsUntilMidnightInTimezone).toHaveBeenCalledWith('America/New_York');
10191019
expect(getSecondsUntilUTCMidnight).not.toHaveBeenCalled();
@@ -1571,16 +1571,16 @@ describe('GET /api/streak', () => {
15711571
});
15721572

15731573
describe('stale-while-revalidate cache header', () => {
1574-
it('contains stale-while-revalidate=60 for normal request', async () => {
1574+
it('contains stale-while-revalidate=59 for normal request', async () => {
15751575
const response = await GET(makeRequest({ user: 'octocat' }));
15761576

1577-
expect(response.headers.get('Cache-Control')).toContain('stale-while-revalidate=60');
1577+
expect(response.headers.get('Cache-Control')).toContain('stale-while-revalidate=59');
15781578
});
15791579

15801580
it('does NOT contain stale-while-revalidate when ?refresh=true', async () => {
15811581
const response = await GET(makeRequest({ user: 'octocat', refresh: 'true' }));
15821582

1583-
expect(response.headers.get('Cache-Control')).not.toContain('stale-while-revalidate=60');
1583+
expect(response.headers.get('Cache-Control')).not.toContain('stale-while-revalidate=59');
15841584
});
15851585
});
15861586

0 commit comments

Comments
 (0)