Skip to content
Closed
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
20 changes: 20 additions & 0 deletions src/app/api/og/[username]/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,26 @@ describe("OG Image Route", () => {
expect(res.status).toBe(429);
expect(res.headers.get("Retry-After")).toBe("0");
});

it("should dedupe concurrent requests", async () => {
const mockFetch = vi.spyOn(global, "fetch").mockImplementation(() => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(new Response(JSON.stringify({ name: "Valid User" }), { status: 200 }));
}, 50);
});
});
Comment on lines +154 to +160

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The mock implementation returns a Promise but is not defined as an async function, and it lacks an explicit return type. To align with the repository's TypeScript guidelines, please update it to be an async function with an explicit return type.

Suggested change
const mockFetch = vi.spyOn(global, "fetch").mockImplementation(() => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(new Response(JSON.stringify({ name: "Valid User" }), { status: 200 }));
}, 50);
});
});
const mockFetch = vi.spyOn(global, "fetch").mockImplementation(async (): Promise<Response> => {
return new Promise<Response>((resolve) => {
setTimeout(() => {
resolve(new Response(JSON.stringify({ name: "Valid User" }), { status: 200 }));
}, 50);
});
});
References
  1. In TypeScript, ensure functions and mock implementations have explicit return types and use async functions for mocks returning Promises to maintain type safety and readability.


const req1 = new NextRequest("http://localhost/api/og/dedupetest");
const req2 = new NextRequest("http://localhost/api/og/dedupetest");

await Promise.all([
GET(req1, { params: Promise.resolve({ username: "dedupetest" }) }),
GET(req2, { params: Promise.resolve({ username: "dedupetest" }) })
]);

expect(mockFetch).toHaveBeenCalledTimes(1);
});
});

function collectText(node: unknown): string[] {
Expand Down
36 changes: 27 additions & 9 deletions src/app/api/og/[username]/route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
import { getClientIp } from "@/lib/rateLimit";

export const runtime = "edge";

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const inflightRequests = new Map<string, Promise<any>>();
const rateLimiter = new RateLimiter(50, 60 * 1000);
const ONE_HOUR_IN_SECONDS = 60 * 60;
const ONE_DAY_IN_SECONDS = 24 * ONE_HOUR_IN_SECONDS;
Expand Down Expand Up @@ -42,15 +45,30 @@
let publicRepos = 0;

try {
const res = await fetch(`https://api.github.com/users/${encodeURIComponent(username)}`, {
headers: {
Accept: "application/vnd.github.v3+json",
"User-Agent": "github-user-summary",
},
next: { revalidate: ONE_DAY_IN_SECONDS },
});
if (res.ok) {
const data = await res.json();
let fetchPromise = inflightRequests.get(username);
if (!fetchPromise) {
fetchPromise = (async () => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Add an explicit return type to the anonymous async function to ensure type safety and adhere to the TypeScript guidelines.

Suggested change
fetchPromise = (async () => {
fetchPromise = (async (): Promise<GitHubProfile | null> => {
References
  1. Maintain explicit return types for functions in TypeScript to ensure type safety and API clarity.

try {
const res = await fetch(`https://api.github.com/users/${encodeURIComponent(username)}`, {
headers: {
Accept: "application/vnd.github.v3+json",
"User-Agent": "github-user-summary",
},
next: { revalidate: ONE_DAY_IN_SECONDS },
});
if (res.ok) {
return await res.json();
}
return null;
Comment on lines +61 to +62

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Shared Failure Result

When the first in-flight GitHub request for a username returns a temporary non-OK response, this promise resolves to null and every concurrent caller renders the fallback OG image. Before this change, those callers fetched independently, so a later concurrent request could still return real profile data instead of sharing the first transient failure.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/api/og/[username]/route.tsx
Line: 61-62

Comment:
**Shared Failure Result**

When the first in-flight GitHub request for a username returns a temporary non-OK response, this promise resolves to `null` and every concurrent caller renders the fallback OG image. Before this change, those callers fetched independently, so a later concurrent request could still return real profile data instead of sharing the first transient failure.

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

} finally {
inflightRequests.delete(username);
}
})();
inflightRequests.set(username, fetchPromise);
}

const data = await fetchPromise;
if (data) {
name = data.name ?? username;
bio = data.bio ?? "";
avatarUrl = data.avatar_url ?? "";
Expand Down Expand Up @@ -84,7 +102,7 @@
}}
>
{avatarUrl && (
<img

Check warning on line 105 in src/app/api/og/[username]/route.tsx

View workflow job for this annotation

GitHub Actions / Lint

Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element
src={sanitizeUrl(avatarUrl)}
alt=""
width={120}
Expand Down
Loading