Skip to content

Commit 3acf881

Browse files
authored
fix(og): stop bypassing cache on every OG image request (JhaSourav07#3412)
## Description Fixes a bug where the OG image endpoint was bypassing cache on every request, firing a fresh GitHub GraphQL call on each link-preview bot crawl (Slack, Discord, Twitter etc.) and silently exhausting API rate limit quota. Closes JhaSourav07#3398 ## Pillar Bug Fix ## Changes Made - Switched `ogParamsSchema.parse()` to `safeParse()` with proper 400 error response - Changed `bypassCache: true` to `bypassCache: refresh` - Added `refresh` field to `ogParamsSchema` - Aligned `Cache-Control` response header with actual fetch behaviour ## Checklist - [x] My code follows the project style guidelines - [x] I have performed a self-review of my code - [x] I have run `npm run lint` and `npm run typecheck` locally - [x] No new errors introduced - [x] Related issue is linked above
2 parents 4c921d7 + e205f2f commit 3acf881

2 files changed

Lines changed: 24 additions & 15 deletions

File tree

app/api/og/route.tsx

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ const displayDomain = (() => {
2121

2222
function getLuminance(hex: string) {
2323
let normalizedHex = hex.trim();
24-
// Normalize short hex (e.g., #fff or #ffff) to #rrggbb (alpha is ignored for luminance)
2524
if (normalizedHex.length === 4 || normalizedHex.length === 5) {
2625
normalizedHex = `#${normalizedHex[1]}${normalizedHex[1]}${normalizedHex[2]}${normalizedHex[2]}${normalizedHex[3]}${normalizedHex[3]}`;
2726
}
@@ -37,12 +36,20 @@ function getLuminance(hex: string) {
3736

3837
export async function GET(req: NextRequest) {
3938
const { searchParams } = new URL(req.url);
40-
const parsed = ogParamsSchema.parse(Object.fromEntries(searchParams.entries()));
41-
let { user } = parsed;
42-
const { theme, bg, text, accent } = parsed;
4339

44-
// Sanitize user: limit to 39 chars (GitHub max length) and strip invalid chars
45-
user = user.slice(0, 39).replace(/[^a-zA-Z0-9-]/g, '');
40+
const parseResult = ogParamsSchema.safeParse(Object.fromEntries(searchParams.entries()));
41+
42+
if (!parseResult.success) {
43+
return new Response(
44+
JSON.stringify({ error: 'Invalid parameters', details: parseResult.error.flatten() }),
45+
{
46+
status: 400,
47+
headers: { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' },
48+
}
49+
);
50+
}
51+
52+
const { user, theme, bg, text, accent, refresh } = parseResult.data;
4653

4754
const selectedTheme = themes[theme] || themes.dark;
4855
const resolvedBg = `#${bg || selectedTheme.bg}`;
@@ -59,19 +66,23 @@ export async function GET(req: NextRequest) {
5966
let longestStreak = 0;
6067
let currentStreak = 0;
6168

62-
// Only the data fetching is wrapped in try/catch — not the JSX rendering.
6369
try {
64-
const userData = await fetchGitHubContributions(user, { bypassCache: true });
65-
const calendar = userData.calendar;
66-
const stats = calculateStreak(calendar);
70+
// bypassCache mirrors the ?refresh=true pattern used by /api/stats and /api/streak.
71+
// Without this, every link-preview bot crawl fires a fresh GitHub GraphQL request,
72+
// burning API quota on an endpoint that is embedded in every page's <meta> tag.
73+
const data = await fetchGitHubContributions(user, { bypassCache: refresh });
74+
const stats = calculateStreak(data.calendar ?? data);
6775
totalCommits = stats.totalContributions;
6876
longestStreak = stats.longestStreak;
6977
currentStreak = stats.currentStreak;
7078
} catch (err) {
7179
console.error('[OG] stats fetch failed:', err);
72-
// fallback to zeros if GitHub is unreachable
7380
}
7481

82+
const cacheControl = refresh
83+
? 'no-cache, no-store, must-revalidate'
84+
: 'public, max-age=3600, stale-while-revalidate=86400';
85+
7586
return new ImageResponse(
7687
<div
7788
style={{
@@ -112,7 +123,6 @@ export async function GET(req: NextRequest) {
112123
{`@${user}`}
113124
</div>
114125
<div style={{ display: 'flex', gap: '48px' }}>
115-
{/* Total Commits */}
116126
<div
117127
style={{
118128
display: 'flex',
@@ -133,7 +143,6 @@ export async function GET(req: NextRequest) {
133143
Total Commits
134144
</div>
135145
</div>
136-
{/* Longest Streak */}
137146
<div
138147
style={{
139148
display: 'flex',
@@ -154,7 +163,6 @@ export async function GET(req: NextRequest) {
154163
{'Longest Streak 🔥'}
155164
</div>
156165
</div>
157-
{/* Current Streak */}
158166
<div
159167
style={{
160168
display: 'flex',
@@ -192,7 +200,7 @@ export async function GET(req: NextRequest) {
192200
width: 1200,
193201
height: 630,
194202
headers: {
195-
'Cache-Control': 'public, max-age=3600, stale-while-revalidate=86400',
203+
'Cache-Control': cacheControl,
196204
},
197205
}
198206
);

lib/validations.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,7 @@ export const ogParamsSchema = z
430430
.optional()
431431
.transform(toEmptyStringAsUndefined)
432432
.transform(toValidHexColor('000000')),
433+
refresh: z.string().optional().transform(toRefreshFlag),
433434
})
434435
.transform((data) => ({
435436
...data,

0 commit comments

Comments
 (0)