cloudflare/cloudflared · error

URL %s has invalid escape characters %s

Error message

URL %s has invalid escape characters %s

What it means

validateUrlString percent-decodes the origin URL with url.PathUnescape before parsing. If the string contains a malformed percent-escape (e.g. a lone '%' not followed by two hex digits), PathUnescape fails and this error is returned. Note the message has a bug: on error unescapedUrl is always the empty string, so the second %s prints nothing useful; the offending input is originUrl.

Source

Thrown at validation/validation.go:100

		return "", fmt.Errorf("URL should not be empty")
	}

	if net.ParseIP(originUrl) != nil {
		return validateIP("", originUrl, "")
	} else if strings.HasPrefix(originUrl, "[") && strings.HasSuffix(originUrl, "]") {
		// ParseIP doesn't recoginze [::1]
		return validateIP("", originUrl[1:len(originUrl)-1], "")
	}

	host, port, err := net.SplitHostPort(originUrl)
	// user might pass in an ip address like 127.0.0.1
	if err == nil && net.ParseIP(host) != nil {
		return validateIP("", host, port)
	}

	unescapedUrl, err := url.PathUnescape(originUrl)
	if err != nil {
		return "", fmt.Errorf("URL %s has invalid escape characters %s", originUrl, unescapedUrl)
	}

	parsedUrl, err := url.Parse(unescapedUrl)
	if err != nil {
		return "", fmt.Errorf("URL %s has invalid format", originUrl)
	}

	// if the url is in the form of host:port, IsAbs() will think host is the schema
	var hostname string
	hasScheme := parsedUrl.IsAbs() && parsedUrl.Host != ""
	if hasScheme {
		err := validateScheme(parsedUrl.Scheme)
		if err != nil {
			return "", err
		}
		// The earlier check for ip address will miss the case http://[::1]
		// and http://[::1]:8080
		if net.ParseIP(parsedUrl.Hostname()) != nil {

View on GitHub (pinned to 2253eeeb25)

Solutions

  1. Fix the URL in your config so every '%' is a valid escape (%XX with hex digits) or encode a literal percent as %25
  2. Check the value actually passed: log or print the originUrl before calling ValidateUrl, since the error's second %s is always empty
  3. If the percent is intentional in a query/path, percent-encode it correctly (50%25) before validation
  4. If you only have a hostname, pass just the hostname instead of a full URL

Example fix

// before
origin := "http://service/api?filter=100%"
ValidateUrl(origin) // invalid escape characters
// after
origin := "http://service/api?filter=100%25"
ValidateUrl(origin)
Defensive patterns

Strategy: validation

Validate before calling

func validEscapes(s string) bool {
    _, err := url.PathUnescape(s)
    return err == nil
}
if !validEscapes(origin) { /* reject before ValidateUrl */ }

Try / catch

if _, err := validation.ValidateUrl(origin); err != nil {
    if strings.Contains(err.Error(), "invalid escape characters") {
        // fall back to percent-encoding literal '%'
    }
}

Prevention

When it happens

Trigger: Calling ValidateUrl or NewAccessValidator with a URL containing an invalid percent sequence such as 'http://example.com/100%' or 'localhost%zz:8080'. Only reached when the input is not a bare IP or host:port IP form.

Common situations: Config file or CLI flag where a URL was copied with a raw '%' (e.g. a query string like '?width=50%' or a password containing '%'), or shell/env interpolation left a stray percent; also URLs built by string concatenation with encoded characters.

Understand the failure class

Background: "Invalid URL" errors: why new URL(), URI.parse, and reqwest::Url reject your string — missing scheme, whitespace, and bad path format — this error's family across 39 libraries.

Related errors


AI-assisted analysis of cloudflare/cloudflared@2253eeeb25 (2026-09-06). Data as JSON: /api/errors/194d168ae7f4ea65. Report an issue: GitHub.