Summary
When a URL passed via the iina://weblink?url=<href> scheme contains characters that are reserved per RFC 3986 but allowed literally per the WHATWG URL spec (most commonly [ and ]), IINA's handler re-encodes the whole url= value before opening it. Characters that were already percent-encoded (%20) get encoded again to %2520, and characters that were literal ([ → %5B) get encoded too. The resulting URL is a different URL than what was passed in, and playback fails.
Modern HTTP tooling (browsers, curl, fetch, Node's URL, mpv) accepts these URLs as-is, so IINA's URL-scheme handler is the odd one out.
Reproduction
Serve any file whose name contains [, ], and a space:
mkdir /tmp/iina-bug && cd /tmp/iina-bug
curl -sLO 'https://archive.org/download/royalty-free-music/A%20Calm%20Hip%20Hop.wav'
mv 'A%20Calm%20Hip%20Hop.wav' '[test] A Calm Hip Hop.wav'
python3 -m http.server 8000
The file is now reachable at:
http://localhost:8000/[test]%20A%20Calm%20Hip%20Hop.wav
That URL plays fine when handed to mpv:
mpv 'http://localhost:8000/[test]%20A%20Calm%20Hip%20Hop.wav'
# -> plays
But fails via IINA's URL scheme:
open 'iina://weblink?url=http://localhost:8000/[test]%20A%20Calm%20Hip%20Hop.wav'
# -> fails
IINA shows the following URL in its UI:
http://localhost:8000/%5Btest%5D%2520A%2520Calm%2520Hip%2520Hop.wav
[ → %5B, ] → %5D, and %20 → %2520 (the % got percent-encoded to %25). The request never even reaches the local server (no hit in the python3 -m http.server access log) because IINA fails on the mangled URL before that.
Note: a URL containing only %20 (no []) does not reproduce — IINA handles it correctly. The trigger is the presence of literal characters that throw off URLComponents.queryItems percent-decoding.
Root cause (from parsePendingURL)
guard let parsed = URLComponents(string: url) else { ... }
guard let queries = parsed.queryItems else { return }
let queryDict = [String: String](uniqueKeysWithValues: queries.map { ($0.name, $0.value ?? "") })
guard let urlValue = queryDict["url"], !urlValue.isEmpty else { ... }
...
player.openURLString(urlValue)
When the inbound URL's url= value contains literal [/], URLComponents.queryItems returns the value without percent-decoding it. So urlValue contains the literal three-character string %20. openURLString then percent-encodes the whole string to turn it into a valid URL, producing %25 for each % (giving %2520) and %5B for each [. Hence the double encoding.
In other words, the handler assumes the url= query parameter is a fully decoded string and re-encodes it — but in practice many callers pass an already-WHATWG-valid URL straight through, which is what the parameter name suggests.
Real-world impact
webtorrent-cli --iina is broken for any torrent whose filenames contain [] (i.e. essentially all scene/release filenames). The same --mpv invocation works fine because mpv is launched with the URL as a CLI argument, not through the URL scheme.
Workaround
Since IINA's handler will re-encode whatever it's given, you can pre-decode the URL once so the re-encoding lands back on the original. Concretely: shadow the open command on PATH with a shim that URL-decodes the url= param of iina://weblink?url=... before forwarding to the real /usr/bin/open, then call your producer (e.g. webtorrent --iina) with that shim on PATH.
The shim:
#!/bin/bash
args=()
for a in "$@"; do
if [[ "\$a" == iina://weblink\?url=* ]]; then
url="\${a#iina://weblink?url=}"
a="iina://weblink?url=\$(python3 -c 'import sys,urllib.parse; print(urllib.parse.unquote(sys.argv[1]))' "\$url")"
fi
args+=("\$a")
done
exec /usr/bin/open "\${args[@]}"
Environment
Summary
When a URL passed via the
iina://weblink?url=<href>scheme contains characters that are reserved per RFC 3986 but allowed literally per the WHATWG URL spec (most commonly[and]), IINA's handler re-encodes the wholeurl=value before opening it. Characters that were already percent-encoded (%20) get encoded again to%2520, and characters that were literal ([→%5B) get encoded too. The resulting URL is a different URL than what was passed in, and playback fails.Modern HTTP tooling (browsers,
curl,fetch, Node'sURL,mpv) accepts these URLs as-is, so IINA's URL-scheme handler is the odd one out.Reproduction
Serve any file whose name contains
[,], and a space:The file is now reachable at:
That URL plays fine when handed to
mpv:But fails via IINA's URL scheme:
IINA shows the following URL in its UI:
[→%5B,]→%5D, and%20→%2520(the%got percent-encoded to%25). The request never even reaches the local server (no hit in thepython3 -m http.serveraccess log) because IINA fails on the mangled URL before that.Note: a URL containing only
%20(no[]) does not reproduce — IINA handles it correctly. The trigger is the presence of literal characters that throw offURLComponents.queryItemspercent-decoding.Root cause (from
parsePendingURL)When the inbound URL's
url=value contains literal[/],URLComponents.queryItemsreturns the value without percent-decoding it. SourlValuecontains the literal three-character string%20.openURLStringthen percent-encodes the whole string to turn it into a validURL, producing%25for each%(giving%2520) and%5Bfor each[. Hence the double encoding.In other words, the handler assumes the
url=query parameter is a fully decoded string and re-encodes it — but in practice many callers pass an already-WHATWG-valid URL straight through, which is what the parameter name suggests.Real-world impact
webtorrent-cli --iinais broken for any torrent whose filenames contain[](i.e. essentially all scene/release filenames). The same--mpvinvocation works fine because mpv is launched with the URL as a CLI argument, not through the URL scheme.Workaround
Since IINA's handler will re-encode whatever it's given, you can pre-decode the URL once so the re-encoding lands back on the original. Concretely: shadow the
opencommand onPATHwith a shim that URL-decodes theurl=param ofiina://weblink?url=...before forwarding to the real/usr/bin/open, then call your producer (e.g.webtorrent --iina) with that shim onPATH.The shim:
Environment