{"record":{"id":"0024841e939ce3ed","repo":"juspay/hyperswitch","slug":"invalid-hex-color-hex","errorCode":null,"errorMessage":"Invalid HEX color: \"${hex}\"","messagePattern":"Invalid HEX color: \"(.+?)\"","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/router/src/core/payment_link/payment_link_initiate/payment_link.js","lineNumber":116,"sourceCode":"    var hex = Math.round(c).toString(16);\n    return hex.length === 1 ? \"0\" + hex : hex;\n  };\n  return \"#\" + toHex(r) + toHex(g) + toHex(b);\n}\n\n/**\n * Ref - https://github.com/onury/invert-color/blob/master/lib/cjs/invert.js\n */\nfunction padz(str, len) {\n  if (len === void 0) {\n    len = 2;\n  }\n  return (new Array(len).join(\"0\") + str).slice(-len);\n}\nfunction hexToRgbArray(hex) {\n  if (hex.slice(0, 1) === \"#\") hex = hex.slice(1);\n  var RE_HEX = /^(?:[0-9a-f]{3}){1,2}$/i;\n  if (!RE_HEX.test(hex)) throw new Error('Invalid HEX color: \"' + hex + '\"');\n  if (hex.length === 3) {\n    hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];\n  }\n  return [\n    parseInt(hex.slice(0, 2), 16),\n    parseInt(hex.slice(2, 4), 16),\n    parseInt(hex.slice(4, 6), 16),\n  ];\n}\nfunction toRgbArray(c) {\n  if (!c) throw new Error(\"Invalid color value\");\n  if (Array.isArray(c)) return c;\n  return typeof c === \"string\" ? hexToRgbArray(c) : [c.r, c.g, c.b];\n}\nfunction getLuminance(c) {\n  var i, x;\n  var a = [];\n  for (i = 0; i < c.length; i++) {","sourceCodeStart":98,"sourceCodeEnd":134,"githubUrl":"https://github.com/juspay/hyperswitch/blob/9b8b89dc378b62c9a6feda647bad4719d2de7699/crates/router/src/core/payment_link/payment_link_initiate/payment_link.js#L98-L134","documentation":"Thrown by hexToRgbArray(), a vendored copy of onury/invert-color embedded in the hosted payment-link page (crates/router/src/core/payment_link/payment_link_initiate/payment_link.js). After stripping an optional leading '#', the string must match /^(?:[0-9a-f]{3}){1,2}$/i, i.e. exactly 3 or 6 hex digits. The page calls invert(primaryColor, true) and invert(chosenColor, true) in initializeEventListeners (lines 330 and 365) to pick a contrasting button text colour, so any merchant branding value that is not a 3/6-digit hex string crashes colour setup for the checkout page.","triggerScenarios":"A payment link initiated with paymentDetails.theme or payment_button_colour set to a CSS keyword ('white'), an rgb()/hsl() string, a 4- or 8-digit hex ('#RGBA', '#RRGGBBAA'), a 5-digit value ('#12345'), a value with surrounding whitespace, or garbage like '#GGGGGG'. The value reaches invert() at line 330 (theme) or 365 (payment_button_colour || theme).","commonSituations":"Merchant branding saved through an API path that never validated the colour format; a client sending 'rgba(0,0,0,1)' or 8-digit alpha hex that CSS accepts but invert-color rejects; DB fields where theme is null being replaced by the literal string 'null'; version drift if branding used to accept named colours.","solutions":["Set the payment link's theme / payment_button_colour to a strict 3- or 6-digit hex string with or without '#', e.g. '#1A2B3C'","Validate the colour at the API boundary where payment link / merchant branding is created, rejecting non-hex values there","In the page JS, default the colour before calling invert(): var primaryColor = paymentDetails.theme || '#000000' and likewise for payment_button_colour","If untrusted values must still render, wrap the invert()/adjustLightness() calls in try/catch and fall back to '#ffffff'/'#000000' so the page still loads","Only widen the regex if you also update adjustLightness(), which makes the same hex assumptions"],"exampleFix":"// before (payment_link.js line 327-330)\nvar primaryColor = paymentDetails.theme;\nvar contrastBWColor = invert(primaryColor, true);\n\n// after\nvar HEX_RE = /^#?(?:[0-9a-f]{3}|[0-9a-f]{6})$/i;\nvar primaryColor =\n  typeof paymentDetails.theme === 'string' && HEX_RE.test(paymentDetails.theme.trim())\n    ? paymentDetails.theme\n    : '#1a2b3c';\nvar contrastBWColor = invert(primaryColor, true);","handlingStrategy":"validation","validationCode":"// Run before using paymentDetails.theme / payment_button_colour\nconst HEX_RE = /^#?(?:[0-9a-f]{3}|[0-9a-f]{6})$/i;\nfunction assertValidHexColor(color, field) {\n  if (typeof color !== 'string' || !HEX_RE.test(color.trim())) {\n    throw new Error(`${field} must be a 3- or 6-digit hex color, got: ${JSON.stringify(color)}`);\n  }\n}\nassertValidHexColor(req.body.theme, 'theme');","typeGuard":"/** @param {unknown} c @returns {c is string} */\nfunction isHexColor(c) {\n  return typeof c === 'string' && /^#?(?:[0-9a-f]{3}|[0-9a-f]{6})$/i.test(c.trim());\n}","tryCatchPattern":"// Last-resort guard around the vendored invert()\ntry {\n  contrastBWColor = invert(primaryColor, true);\n} catch (e) {\n  if (/Invalid HEX color/.test(e.message)) {\n    contrastBWColor = '#ffffff'; // sane default, page still renders\n  } else {\n    throw e;\n  }\n}","preventionTips":["Validate branding colours as strict hex at the API boundary where payment links / merchant profiles are created","Never accept CSS named colours, rgb()/hsl(), or 8-digit alpha hex in fields destined for invert()","Keep the vendored invert-color copies in payment_link.js and status.js in sync when patching","Add a unit test asserting the page renders with theme unset and with '#ABC' shorthand"],"tags":["payment-link","branding","hex-color","validation","client-side"],"backgroundTag":null,"analyzedSha":"9b8b89dc378b62c9a6feda647bad4719d2de7699","analyzedAt":"2026-08-16T09:01:53.433Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}