jdx/mise · error

invalid relay path encoding

Error message

invalid relay path encoding

What it means

`validate_path` percent-decodes each segment of the relayed URL path. Before decoding it checks that every `%` is followed by exactly two ASCII hex digits; a bare or malformed `%` (e.g. `100%`, `%ZZ`) makes the segment undecodable and fails with this error, preventing ambiguous or smuggling-style paths from reaching the upstream host.

Source

Thrown at src/github_relay.rs:180

struct Target {
    url: String,
    git: bool,
    archive_repo: Option<String>,
}

#[cfg(any(unix, test))]
fn validate_path(path: &str) -> Result<()> {
    // Validate decoded segments, retaining the original spelling upstream. Reject
    // encoded separators and percent signs so a second decoder cannot change scope.
    for segment in path.split('/') {
        for (index, byte) in segment.bytes().enumerate() {
            if byte == b'%'
                && !segment
                    .as_bytes()
                    .get(index + 1..index + 3)
                    .is_some_and(|digits| digits.iter().all(u8::is_ascii_hexdigit))
            {
                bail!("invalid relay path encoding");
            }
        }
        let decoded = urlencoding::decode(segment)?;
        if decoded.is_empty()
            || matches!(decoded.as_ref(), "." | "..")
            || decoded.contains(['/', '\\', '%'])
            || decoded.chars().any(char::is_control)
        {
            bail!("invalid relay path");
        }
    }
    Ok(())
}

#[cfg(any(unix, test))]
fn authorize(scope: &Scope, method: &str, path: &str, query: Option<&str>) -> Result<Target> {
    validate_path(path)?;
    let p: Vec<_> = path.split('/').collect();

View on GitHub (pinned to afd2eddd3a)

Solutions

  1. Percent-encode the segment properly with `urlencoding::encode` (a literal `%` becomes `%25`).
  2. Remove or rename the ref/tag containing the stray `%` character.
  3. Verify the URL wasn't double-encoded; decode once yourself before passing the path.

Example fix

// before
let path = format!("/api/repos/{owner}/{repo}/tarball/{tag}"); // tag = "v1.0%"
// after
let path = format!("/api/repos/{owner}/{repo}/tarball/{}", urlencoding::encode(tag));
Defensive patterns

Strategy: validation

Validate before calling

fn segment_encodes_cleanly(seg: &str) -> bool {
    let b = seg.as_bytes();
    (0..b.len()).all(|i| b[i] != b'%' || (i + 2 < b.len()
        && b[i+1].is_ascii_hexdigit() && b[i+2].is_ascii_hexdigit()))
}
let encoded = urlencoding::encode(raw_segment); // encode % as %25 before building the path

Try / catch

match relay::authorize(&scope, "GET", path, None) {
    Err(e) if e.to_string().contains("path encoding") => eprintln!("percent-encode segment: {e}"),
    Err(e) => return Err(e),
    Ok(t) => t,
}

Prevention

When it happens

Trigger: Calling `authorize` or `archive_redirect` with a path segment containing `%` not forming a valid percent-escape — e.g. `/api/repos/owner/repo/tarball/100%done` or `/git/owner/repo.git/info%2refs`.

Common situations: Refs, tags, or file names containing literal `%` that were not percent-encoded (`v1.0%`); double-encoded values pasted from logs (`%2520`); building the path with an unencoded user-supplied string.

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 jdx/mise@afd2eddd3a (2026-09-09). Data as JSON: /api/errors/c5fdcbb3bfb7150b. Report an issue: GitHub.