juspay/hyperswitch · error

Invalid HEX color: "${hex}"

Error message

Invalid HEX color: "${hex}"

What it means

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.

Source

Thrown at crates/router/src/core/payment_link/payment_link_initiate/payment_link.js:116

    var hex = Math.round(c).toString(16);
    return hex.length === 1 ? "0" + hex : hex;
  };
  return "#" + toHex(r) + toHex(g) + toHex(b);
}

/**
 * Ref - https://github.com/onury/invert-color/blob/master/lib/cjs/invert.js
 */
function padz(str, len) {
  if (len === void 0) {
    len = 2;
  }
  return (new Array(len).join("0") + str).slice(-len);
}
function hexToRgbArray(hex) {
  if (hex.slice(0, 1) === "#") hex = hex.slice(1);
  var RE_HEX = /^(?:[0-9a-f]{3}){1,2}$/i;
  if (!RE_HEX.test(hex)) throw new Error('Invalid HEX color: "' + hex + '"');
  if (hex.length === 3) {
    hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
  }
  return [
    parseInt(hex.slice(0, 2), 16),
    parseInt(hex.slice(2, 4), 16),
    parseInt(hex.slice(4, 6), 16),
  ];
}
function toRgbArray(c) {
  if (!c) throw new Error("Invalid color value");
  if (Array.isArray(c)) return c;
  return typeof c === "string" ? hexToRgbArray(c) : [c.r, c.g, c.b];
}
function getLuminance(c) {
  var i, x;
  var a = [];
  for (i = 0; i < c.length; i++) {

View on GitHub (pinned to 9b8b89dc37)

Solutions

  1. Set the payment link's theme / payment_button_colour to a strict 3- or 6-digit hex string with or without '#', e.g. '#1A2B3C'
  2. Validate the colour at the API boundary where payment link / merchant branding is created, rejecting non-hex values there
  3. In the page JS, default the colour before calling invert(): var primaryColor = paymentDetails.theme || '#000000' and likewise for payment_button_colour
  4. 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
  5. Only widen the regex if you also update adjustLightness(), which makes the same hex assumptions

Example fix

// before (payment_link.js line 327-330)
var primaryColor = paymentDetails.theme;
var contrastBWColor = invert(primaryColor, true);

// after
var HEX_RE = /^#?(?:[0-9a-f]{3}|[0-9a-f]{6})$/i;
var primaryColor =
  typeof paymentDetails.theme === 'string' && HEX_RE.test(paymentDetails.theme.trim())
    ? paymentDetails.theme
    : '#1a2b3c';
var contrastBWColor = invert(primaryColor, true);
Defensive patterns

Strategy: validation

Validate before calling

// Run before using paymentDetails.theme / payment_button_colour
const HEX_RE = /^#?(?:[0-9a-f]{3}|[0-9a-f]{6})$/i;
function assertValidHexColor(color, field) {
  if (typeof color !== 'string' || !HEX_RE.test(color.trim())) {
    throw new Error(`${field} must be a 3- or 6-digit hex color, got: ${JSON.stringify(color)}`);
  }
}
assertValidHexColor(req.body.theme, 'theme');

Type guard

/** @param {unknown} c @returns {c is string} */
function isHexColor(c) {
  return typeof c === 'string' && /^#?(?:[0-9a-f]{3}|[0-9a-f]{6})$/i.test(c.trim());
}

Try / catch

// Last-resort guard around the vendored invert()
try {
  contrastBWColor = invert(primaryColor, true);
} catch (e) {
  if (/Invalid HEX color/.test(e.message)) {
    contrastBWColor = '#ffffff'; // sane default, page still renders
  } else {
    throw e;
  }
}

Prevention

When it happens

Trigger: 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).

Common situations: 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.

Related errors


AI-assisted analysis of juspay/hyperswitch@9b8b89dc37 (2026-08-16). Data as JSON: /api/errors/0024841e939ce3ed. Report an issue: GitHub.