evanw/esbuild · error

could not decode percent-escaped data: %s

Error message

could not decode percent-escaped data: %s

What it means

Returned by DataURL.DecodeData when a non-base64 data URL's payload fails net/url.PathUnescape. Percent-escaped data URLs use %XX sequences; an invalid sequence (e.g. '%ZZ' or a trailing '%') makes PathUnescape error. esbuild hits this while resolving data: imports.

Source

Thrown at internal/resolver/dataurl.go:73

	default:
		return MIMETypeUnsupported
	}
}

func (parsed DataURL) DecodeData() (string, error) {
	// Try to read base64 data
	if parsed.isBase64 {
		bytes, err := base64.StdEncoding.DecodeString(parsed.data)
		if err != nil {
			return "", fmt.Errorf("could not decode base64 data: %s", err.Error())
		}
		return string(bytes), nil
	}

	// Try to read percent-escaped data
	content, err := url.PathUnescape(parsed.data)
	if err != nil {
		return "", fmt.Errorf("could not decode percent-escaped data: %s", err.Error())
	}
	return content, nil
}

View on GitHub (pinned to 6ff1d8b0d8)

Solutions

  1. Properly percent-encode the payload: every literal '%' must become '%25'.
  2. If the data is binary or contains many special chars, switch to ';base64' encoding instead.
  3. Generate the data URL with encodeURIComponent (JS) or url.PathEscape (Go) rather than concatenating raw strings.
  4. Lint data URLs in fixtures with a small decoder test before bundling.

Example fix

// before
import x from 'data:text/plain,50%off'

// after
import x from 'data:text/plain,' + encodeURIComponent('50%off')  // -> '50%25off'
Defensive patterns

Strategy: validation

Validate before calling

// Check percent-encoding of a non-base64 data URL.
function checkPercentDataUrl(url) {
  if (!url.startsWith('data:') || url.includes(';base64,')) return true
  const payload = url.slice(url.indexOf(',') + 1)
  return !/(?:%[0-9A-Fa-f]{2})|[%]/.test(payload.replace(/%[0-9A-Fa-f]{2}/g, ''))
}
if (!checkPercentDataUrl(specifier)) {
  // re-encode properly:
  specifier = 'data:' + mime + ',' + encodeURIComponent(raw)
}

Type guard

function isFullyPercentEncoded(payload: string): boolean {
  // after stripping valid %XX, no stray '%' should remain
  return !payload.replace(/%[0-9A-Fa-f]{2}/g, '').includes('%')
}

Try / catch

try {
  await esbuild.build({ ... })
} catch (e) {
  if (/could not decode percent-escaped data/i.test(e.message)) {
    console.error('A data URL import has invalid percent-encoding:', e.message)
  }
  throw e
}

Prevention

When it happens

Trigger: Import a data URL like `data:text/plain,100%25` (fine) but break it with `data:text/plain,50%off` (the '%of' is not a valid hex pair). Any data URL without ';base64' whose percent sequences are malformed triggers this.

Common situations: URL-encoding a literal '%' as a single '%' instead of '%25'; templating engines that interpolate user text into a data URL without escaping; truncation that leaves a dangling '%'.

Related errors


AI-assisted analysis of evanw/esbuild@6ff1d8b0d8 (2026-08-03). Data as JSON: /data/errors/0c8ef6dcd3a50ffa.json. Report an issue: GitHub.