Skip to content

iina://weblink?url=... double-encodes URLs containing reserved characters (e.g. [, ]) #6110

@fiocchidavide

Description

@fiocchidavide

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

  • IINA 1.4.3
  • macOS 26.5

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions