From dae53cc62443435fb7f6504d3ecd85a6e578f781 Mon Sep 17 00:00:00 2001 From: anonymousRecords Date: Sat, 29 Aug 2026 18:16:27 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=EC=8B=A4=ED=8C=A8=20=EC=9B=90=EC=9D=B8?= =?UTF-8?q?=EB=B3=84=EB=A1=9C=20HTTP=20=EC=83=81=ED=83=9C=EC=BD=94?= =?UTF-8?q?=EB=93=9C=EB=A5=BC=20=EA=B5=AC=EB=B6=84=ED=95=B4=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 지금까지 모든 실패가 500으로 뭉개져, 사용자 오타인지 GitHub 장애인지 배포 설정 오류인지 로그를 봐야만 알 수 있었다. 에러 메시지 문자열로 분기하면 문구 변경에 깨지므로 타입을 도입했다. - UserNotFoundError → 404. GraphQL errors의 type이 NOT_FOUND인 경우와 data.user가 null인 경우를 여기에 매핑한다. - GitHubApiError → 502. HTTP 실패와 NOT_FOUND 외 GraphQL 에러. 업스트림 장애를 우리 쪽 오류(500)와 구분하기 위함이다. - MissingTokenError → 500. 배포 설정 오류이므로 서버 오류가 맞다. 에러 응답에는 캐시 헤더를 붙이지 않는다. 성공 응답은 s-maxage=86400이라 오타 친 사용자명이 하루 동안 캐시되면 곤란하다. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01La1oXcrwEZycnLJGdkFn3y --- app/api/[username]/animation/route.ts | 14 ++++++++++++++ lib/api/github.ts | 27 ++++++++++++++++++++------- 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/app/api/[username]/animation/route.ts b/app/api/[username]/animation/route.ts index 3088bb8..febda30 100644 --- a/app/api/[username]/animation/route.ts +++ b/app/api/[username]/animation/route.ts @@ -1,4 +1,5 @@ import { type NextRequest, NextResponse } from "next/server"; +import { GitHubApiError, UserNotFoundError } from "@/lib/api/github"; import { generateFlowerAPNG } from "@/lib/themes/flower/generator"; import { generateHairAPNG } from "@/lib/themes/hair/generator"; import type { FlowerType, HairCurliness } from "@/lib/themes/types"; @@ -102,6 +103,19 @@ export async function GET( }); } catch (error) { console.error("[APNG GENERATION ERROR]", error); + + if (error instanceof UserNotFoundError) { + return new NextResponse(`GitHub user not found: ${username}`, { + status: 404, + }); + } + + if (error instanceof GitHubApiError) { + return new NextResponse("Failed to reach the GitHub API", { + status: 502, + }); + } + return new NextResponse("Internal Server Error", { status: 500 }); } } diff --git a/lib/api/github.ts b/lib/api/github.ts index 8d8ed79..784a0aa 100644 --- a/lib/api/github.ts +++ b/lib/api/github.ts @@ -1,3 +1,12 @@ +/** 요청한 GitHub 사용자가 존재하지 않음. */ +export class UserNotFoundError extends Error {} + +/** GitHub API 호출 자체가 실패함 (업스트림 장애). */ +export class GitHubApiError extends Error {} + +/** GITHUB_TOKEN 환경변수가 설정되지 않음 (배포 설정 오류). */ +export class MissingTokenError extends Error {} + export interface ContributionDay { date: string; count: number; @@ -22,7 +31,7 @@ interface ContributionsQueryResponse { }; } | null; }; - errors?: { message: string }[]; + errors?: { message: string; type?: string }[]; } const CONTRIBUTIONS_QUERY = ` @@ -48,7 +57,7 @@ export async function fetchContributions( const token = process.env.GITHUB_TOKEN; if (!token) { - throw new Error("GITHUB_TOKEN is not configured"); + throw new MissingTokenError("GITHUB_TOKEN is not configured"); } const res = await fetch("https://api.github.com/graphql", { @@ -64,7 +73,7 @@ export async function fetchContributions( }); if (!res.ok) { - throw new Error( + throw new GitHubApiError( `Failed to fetch contributions for ${username} (HTTP ${res.status})`, ); } @@ -72,16 +81,20 @@ export async function fetchContributions( const json: ContributionsQueryResponse = await res.json(); if (json.errors?.length) { - throw new Error( - `Failed to fetch contributions for ${username}: ${json.errors[0].message}`, - ); + const message = `Failed to fetch contributions for ${username}: ${json.errors[0].message}`; + + if (json.errors.some((error) => error.type === "NOT_FOUND")) { + throw new UserNotFoundError(message); + } + + throw new GitHubApiError(message); } const rawWeeks = json.data?.user?.contributionsCollection?.contributionCalendar?.weeks; if (!rawWeeks) { - throw new Error(`GitHub user not found: ${username}`); + throw new UserNotFoundError(`GitHub user not found: ${username}`); } const days: ContributionDay[] = rawWeeks