66from config .settings import get_settings
77from generators import stats_card , lang_card , contrib_card , recent_activity_card , trophy_card , streak_card , repo_card , social_card , badge_generator
88from utils import github_api
9+ from utils .error_card import draw_error_card
910from utils .cache import cache_svg_response , get_cache_stats , clear_cache
1011from utils .validators import (
1112 validate_date ,
@@ -45,7 +46,7 @@ def generate_cached_svg(generator_func, *args, **kwargs):
4546 return generator_func (* args , ** kwargs )
4647
4748
48- def svg_response (svg_content : str , request : Request ):
49+ def svg_response (svg_content : str , request : Request , max_age : int = 14400 ):
4950 etag = hashlib .md5 (svg_content .encode ("utf-8" )).hexdigest ()
5051
5152 if request .headers .get ("if-none-match" ) == etag :
@@ -55,7 +56,7 @@ def svg_response(svg_content: str, request: Request):
5556 content = svg_content ,
5657 media_type = "image/svg+xml" ,
5758 headers = {
58- "Cache-Control" : "public, max-age=14400 , s-maxage=14400 " ,
59+ "Cache-Control" : f "public, max-age={ max_age } , s-maxage={ max_age } " ,
5960 "ETag" : etag ,
6061 "Vary" : "Accept-Encoding" ,
6162 # Security headers to prevent XSS
@@ -190,6 +191,22 @@ def parse_heatmap_colors(level_0, level_1, level_2, level_3, level_4):
190191
191192 return colors if colors else None
192193
194+
195+ def fetch_github_data_or_error_svg (request : Request , username : str , token : Optional [str ] = None ):
196+ """Fetch GitHub profile data and return an SVG fallback response on API failures."""
197+ try :
198+ data = github_api .get_live_github_data (username , token , raise_errors = True )
199+ return data , None
200+ except github_api .UserNotFoundError as exc :
201+ svg_content = draw_error_card ("user_not_found" , username = username , message = exc .message )
202+ except github_api .RateLimitExceededError as exc :
203+ svg_content = draw_error_card ("rate_limited" , username = username , message = exc .message )
204+ except github_api .GitHubApiError as exc :
205+ svg_content = draw_error_card ("api_error" , username = username , message = exc .message )
206+
207+ # Keep error card cache shorter than normal cards so transient API issues recover quickly.
208+ return None , svg_response (svg_content , request , max_age = 300 )
209+
193210@app .get ("/api/stats" )
194211async def get_stats (
195212 request : Request ,
@@ -212,8 +229,10 @@ async def get_stats(
212229
213230 # Get optional token from Authorization header for higher rate limits
214231 token = get_token_from_header (request )
215-
216- data = github_api .get_live_github_data (username , token ) or github_api .get_mock_data (username )
232+
233+ data , error_response = fetch_github_data_or_error_svg (request , username , token )
234+ if error_response :
235+ return error_response
217236
218237 show_options = {
219238 "stars" : not hide_stars ,
@@ -244,7 +263,10 @@ async def get_languages(
244263 username = validate_username (username )
245264 theme = validate_theme (theme )
246265
247- data = github_api .get_live_github_data (username ) or github_api .get_mock_data (username )
266+ token = get_token_from_header (request )
267+ data , error_response = fetch_github_data_or_error_svg (request , username , token )
268+ if error_response :
269+ return error_response
248270 custom_colors = parse_custom_overrides (bg_color , title_color , text_color , border_color , font )
249271
250272 # Parse exclude parameter into list of languages
@@ -279,7 +301,10 @@ async def get_contributions(
279301 start_date = validate_date (start_date )
280302 end_date = validate_date (end_date )
281303
282- data = github_api .get_live_github_data (username ) or github_api .get_mock_data (username )
304+ token = get_token_from_header (request )
305+ data , error_response = fetch_github_data_or_error_svg (request , username , token )
306+ if error_response :
307+ return error_response
283308 custom_colors = parse_custom_overrides (bg_color , title_color , text_color , border_color , font )
284309
285310 # Build date_range dict if dates are provided
@@ -320,8 +345,11 @@ async def get_calendar_heatmap(
320345 end_date = validate_date (end_date )
321346
322347 token = get_token_from_header (request )
323- data = github_api .get_live_github_data (username , token ) or github_api .get_mock_data (username )
324- custom_colors = parse_colors (bg_color , title_color , text_color , border_color )
348+ data , error_response = fetch_github_data_or_error_svg (request , username , token )
349+ if error_response :
350+ return error_response
351+
352+ custom_colors = parse_custom_overrides (bg_color , title_color , text_color , border_color )
325353 heatmap_colors = parse_heatmap_colors (level_0 , level_1 , level_2 , level_3 , level_4 )
326354
327355 date_range = None
@@ -384,7 +412,10 @@ async def get_trophy(
384412 username = validate_username (username )
385413 theme = validate_theme (theme )
386414
387- data = github_api .get_live_github_data (username ) or github_api .get_mock_data (username )
415+ token = get_token_from_header (request )
416+ data , error_response = fetch_github_data_or_error_svg (request , username , token )
417+ if error_response :
418+ return error_response
388419 custom_colors = parse_custom_overrides (bg_color , title_color , text_color , border_color , font )
389420 svg_content = trophy_card .draw_trophy_card (data , theme , custom_colors = custom_colors )
390421 return svg_response (svg_content , request )
@@ -405,7 +436,10 @@ async def get_streak(
405436 username = validate_username (username )
406437 theme = validate_theme (theme )
407438
408- data = github_api .get_live_github_data (username ) or github_api .get_mock_data (username )
439+ token = get_token_from_header (request )
440+ data , error_response = fetch_github_data_or_error_svg (request , username , token )
441+ if error_response :
442+ return error_response
409443 custom_colors = parse_custom_overrides (bg_color , title_color , text_color , border_color , font )
410444 svg_content = streak_card .draw_streak_card (data , theme , custom_colors = custom_colors )
411445 return svg_response (svg_content , request )
@@ -430,7 +464,10 @@ async def get_repos(
430464 sort_by = validate_sort_by (sort_by )
431465 limit = validate_limit (limit , min_val = 1 , max_val = 10 )
432466
433- data = github_api .get_live_github_data (username ) or github_api .get_mock_data (username )
467+ token = get_token_from_header (request )
468+ data , error_response = fetch_github_data_or_error_svg (request , username , token )
469+ if error_response :
470+ return error_response
434471 custom_colors = parse_custom_overrides (bg_color , title_color , text_color , border_color , font )
435472 svg_content = repo_card .draw_repo_card (data , theme , custom_colors = custom_colors , sort_by = sort_by , limit = limit )
436473 return svg_response (svg_content , request )
0 commit comments