jdx/mise · error

invalid relay path encoding

Error message

invalid relay path encoding

What it means

`validate_path` rejects relay request paths containing a `%` that is not followed by two hex digits (or the `%` sits at the very end of a segment). Because the relay later percent-decodes segments, a malformed escape would either fail decoding or allow a second decoder downstream to reinterpret the path, so it is rejected up front as a security guard against scope bypass.

Solutions

  1. Percent-encode the path properly so every `%` becomes `%25` and real escapes use two hex digits
  2. Remove or escape the stray `%` character from the requested path/ref/filename
  3. If a proxy rewrote the URL, disable the rewriting or re-encode the path before it reaches the relay

Example fix

// before
let path = "/web/org/repo/archive/refs/tags/v1.0%.tar.gz";
// after
let path = "/web/org/repo/archive/refs/tags/v1.0%25.tar.gz";
Defensive patterns

Strategy: validation

Validate before calling

fn has_valid_encoding(path: &str) -> bool {
    path.split('/').all(|seg| {
        seg.bytes().enumerate().all(|(i, b)| {
            b != b'%' || seg.as_bytes().get(i+1..i+3)
                .is_some_and(|d| d.iter().all(u8::is_ascii_hexdigit))
        })
    })
}

Try / catch

match authorize(&scope, method, path, query) {
    Err(e) if e.to_string().contains("invalid relay path encoding") => {
        eprintln!("path contains a malformed % escape: {path}");
    }
    Ok(t) => forward(t),
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Any relayed request path segment containing a bare `%` not forming a valid `%XX` escape: e.g. `/git/org/repo/info/refs` with a literal `%` in a ref name, a truncated escape like `%A`, `%ZZ`, or a `%` at the end of a segment such as `/api/repos/org/re%`.

Common situations: A tool or script passes a raw filename containing `%` (common in generated URLs, logs, or Windows-encoded names) into an archive or contents path; a truncated double-encoding (`%25` became `%`) after an intermediate proxy rewrote the URL.

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

Appendix: 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 533346cc37)