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
- Properly percent-encode the payload: every literal '%' must become '%25'.
- If the data is binary or contains many special chars, switch to ';base64' encoding instead.
- Generate the data URL with encodeURIComponent (JS) or url.PathEscape (Go) rather than concatenating raw strings.
- 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
- Always encodeURIComponent user data before embedding in a data URL.
- Escape literal '%' as '%25'.
- Consider base64 for binary payloads to sidestep percent-encoding edge cases.
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.