@@ -30,6 +30,7 @@ import {
3030 generateSkylineSVG ,
3131 generateLanguagesSVG ,
3232 generateActivityGraphSVG ,
33+ buildInlineErrorSVG ,
3334} from '@/lib/svg/generator' ;
3435import { generateConstellationSVG } from '@/lib/svg/constellation' ;
3536import { generateRadarSVG } from '@/lib/svg/radar' ;
@@ -46,7 +47,7 @@ import type {
4647 ContributionCalendar ,
4748 StreakStats ,
4849} from '@/types' ;
49- import { getNormalizedThemeKey , themes } from '@/lib/svg/themes' ;
50+ import { getNormalizedThemeKey , themes , resolveErrorTheme } from '@/lib/svg/themes' ;
5051import { streakParamsSchema , coerceQueryParams } from '@/lib/validations' ;
5152import { sanitizeHexColor , sanitizeRadius , escapeXML } from '@/lib/svg/sanitizer' ;
5253import { getClientIp } from '@/utils/getClientIp' ;
@@ -62,26 +63,6 @@ const validationCache = _vc;
6263const SVG_CSP_HEADER =
6364 "default-src 'none'; style-src 'unsafe-inline' https://fonts.googleapis.com; font-src https://fonts.gstatic.com; connect-src https://fonts.gstatic.com;" ;
6465
65- function buildInlineErrorSVG ( text : string ) : string {
66- const MAX_LINE = 48 ;
67- const chars = Array . from ( text ) ;
68- const truncated =
69- chars . length > MAX_LINE * 2 ? chars . slice ( 0 , MAX_LINE * 2 - 1 ) . join ( '' ) + '…' : text ;
70- const truncatedChars = Array . from ( truncated ) ;
71- const line1 = escapeXML ( truncatedChars . slice ( 0 , MAX_LINE ) . join ( '' ) ) ;
72- const line2 =
73- truncatedChars . length > MAX_LINE ? escapeXML ( truncatedChars . slice ( MAX_LINE ) . join ( '' ) ) : null ;
74- const textY = line2 ? '62' : '75' ;
75- return `<svg xmlns="http://www.w3.org/2000/svg" width="400" height="150" viewBox="0 0 400 150">
76- <rect width="400" height="150" fill="#2d0000" rx="8"/>
77- <text x="200" y="${ textY } " text-anchor="middle" dominant-baseline="central" fill="#ffcccc" font-family="sans-serif" font-size="13">${ line1 } </text>${
78- line2
79- ? `\n <text x="200" y="91" text-anchor="middle" dominant-baseline="central" fill="#ffcccc" font-family="sans-serif" font-size="13">${ line2 } </text>`
80- : ''
81- }
82- </svg>` ;
83- }
84-
8566function getMonthlyReferenceDate ( year : string | undefined , timezone : string ) : Date | undefined {
8667 if ( ! year ) return undefined ;
8768
@@ -117,7 +98,13 @@ export async function GET(request: Request) {
11798 Object . values ( fieldErrors . fieldErrors ) . flat ( ) [ 0 ] ??
11899 fieldErrors . formErrors [ 0 ] ??
119100 'Invalid parameters' ;
120- const errorSvg = buildInlineErrorSVG ( firstError ) ;
101+ const errTheme = resolveErrorTheme ( searchParams ) ;
102+ const errorSvg = buildInlineErrorSVG ( firstError , {
103+ bg : errTheme . bg ,
104+ accent : errTheme . accent ,
105+ text : errTheme . text ,
106+ radius : errTheme . radius ,
107+ } ) ;
121108 return new NextResponse ( errorSvg , {
122109 status : 400 ,
123110 headers : {
@@ -775,7 +762,7 @@ export async function GET(request: Request) {
775762 } ,
776763 } ) ;
777764 } catch ( error : unknown ) {
778- return buildErrorResponse ( error , parseResult , requestId ) ;
765+ return buildErrorResponse ( error , parseResult , requestId , request ) ;
779766 }
780767}
781768
@@ -813,7 +800,8 @@ function sanitizeErrorMessage(message: string): string {
813800function buildErrorResponse (
814801 error : unknown ,
815802 parseResult : ParseResult ,
816- requestId ?: string
803+ requestId ?: string ,
804+ request ?: Request
817805) : NextResponse {
818806 const rawMessage = error instanceof Error ? error . message : String ( error ) ;
819807 const message = sanitizeErrorMessage ( rawMessage ) ;
@@ -869,17 +857,28 @@ function buildErrorResponse(
869857 rawMessage . toLowerCase ( ) . includes ( 'validation' ) ||
870858 rawMessage . toLowerCase ( ) . includes ( 'strictly for organizations' ) ;
871859
872- const errBg = `#${ sanitizeHexColor ( parseResult . success ? parseResult . data . bg : undefined , '0d1117' ) } ` ;
860+ const searchParams = request ? new URL ( request . url ) . searchParams : undefined ;
861+ const errTheme = resolveErrorTheme ( searchParams ) ;
862+ const errBg =
863+ parseResult . success && parseResult . data . bg
864+ ? `#${ sanitizeHexColor ( parseResult . data . bg , '0d1117' ) } `
865+ : errTheme . bg ;
873866 const errAccentRaw =
874867 ( parseResult . success &&
875868 ( Array . isArray ( parseResult . data . accent )
876869 ? parseResult . data . accent [ parseResult . data . accent . length - 1 ]
877870 : parseResult . data . accent ) ) ||
878871 undefined ;
879- const errAccent = `#${ sanitizeHexColor ( errAccentRaw , '58a6ff' ) } ` ;
880- const errText = `#${ sanitizeHexColor ( parseResult . success ? parseResult . data . text : undefined , 'c9d1d9' ) } ` ;
881- const errRadius = sanitizeRadius ( parseResult . success ? parseResult . data . radius : undefined , 8 ) ;
882- const errSpeed = ( parseResult . success && parseResult . data . speed ) || '8s' ;
872+ const errAccent = errAccentRaw ? `#${ sanitizeHexColor ( errAccentRaw , '58a6ff' ) } ` : errTheme . accent ;
873+ const errText =
874+ parseResult . success && parseResult . data . text
875+ ? `#${ sanitizeHexColor ( parseResult . data . text , 'c9d1d9' ) } `
876+ : errTheme . text ;
877+ const errRadius =
878+ parseResult . success && parseResult . data . radius !== undefined
879+ ? sanitizeRadius ( parseResult . data . radius , 8 )
880+ : errTheme . radius ;
881+ const errSpeed = ( parseResult . success && parseResult . data . speed ) || errTheme . speed ;
883882
884883 if ( isRateLimit ) {
885884 const telemetry = getCircuitTelemetry ( ) ;
@@ -931,7 +930,12 @@ function buildErrorResponse(
931930
932931 // 3. Return a 400 Bad Request for Validation Errors
933932 if ( isValidationError ) {
934- const validationSvg = buildInlineErrorSVG ( message ) ;
933+ const validationSvg = buildInlineErrorSVG ( message , {
934+ bg : errBg ,
935+ accent : errAccent ,
936+ text : errText ,
937+ radius : errRadius ,
938+ } ) ;
935939 const errorHeaders : Record < string , string > = {
936940 'Content-Type' : 'image/svg+xml; charset=utf-8' ,
937941 'Cache-Control' : 'no-store' ,
@@ -948,7 +952,12 @@ function buildErrorResponse(
948952
949953 // 4. Return a 504 Gateway Timeout for aborted/timed out requests
950954 if ( isAbortError ( error ) ) {
951- const timeoutSvg = buildInlineErrorSVG ( 'Request timed out. Please try again later.' ) ;
955+ const timeoutSvg = buildInlineErrorSVG ( 'Request timed out. Please try again later.' , {
956+ bg : errBg ,
957+ accent : errAccent ,
958+ text : errText ,
959+ radius : errRadius ,
960+ } ) ;
952961 const errorHeaders : Record < string , string > = {
953962 'Content-Type' : 'image/svg+xml; charset=utf-8' ,
954963 'Cache-Control' : 'no-store' ,
@@ -969,7 +978,12 @@ function buildErrorResponse(
969978 message,
970979 } ) ;
971980
972- const errorSvg = buildInlineErrorSVG ( 'Something went wrong. Please try again later.' ) ;
981+ const errorSvg = buildInlineErrorSVG ( 'Something went wrong. Please try again later.' , {
982+ bg : errBg ,
983+ accent : errAccent ,
984+ text : errText ,
985+ radius : errRadius ,
986+ } ) ;
973987 const errorHeaders : Record < string , string > = {
974988 'Content-Type' : 'image/svg+xml; charset=utf-8' ,
975989 'Cache-Control' : 'no-store' ,
0 commit comments