Hrefine

Muscle memory for AI. Solve it once in Temper, and every language remembers.

What host is this URL for?

https://example.com\@evil.example/

  • Your browserexample.comthe standard's answer
  • Hrefine, in all seven languagesexample.com same as the browser
  • Python urllibevil.example differs from the browser
  • curlevil.example differs from the browser
  • Java URIerror rejects it
  • Java URLerror rejects it
  • C# Urierror rejects it
  • Go net/urlerror rejects it
How each parser reads it
ParserHostComparedReads the URL as
Browsersexample.comhttps://example.com/@evil.example/
Hrefineexample.com same as the browserhttps://example.com/@evil.example/
Python urllibevil.example differs from the browserhttps://example.com\@evil.example/
Java URIerror rejects itURISyntaxException: Illegal character in authority at index 19: https://example.com\@evil.example/
Java URLerror rejects itMalformedURLException: Illegal character found in user-info: '\'
C# Urierror rejects itUriFormatException: Invalid URI: The hostname could not be parsed.
Go net/urlerror rejects itparse "https://example.com\\@evil.example/": net/url: invalid userinfo
curlevil.example differs from the browserhttps://example.com\@evil.example/

If your server checks one answer and fetches another, an attacker picks which one.

Hrefine reads URLs as the WHATWG URL Standard says, which browsers follow, with the same answers in C++, C#, Java, JavaScript, Lua, Python, and Rust. Every web-platform-tests case passes in all seven.

Try to break it

Type any URL. Hrefine parses it live in this page, with the same JavaScript build its tests run on, and your browser's own URL parses it beside it.

These fields can't hold tabs or newlines, so ␉ and ␊ stand in for them; elsewhere they show as \t and \n.

Hrefine live

Your browser's URL live

Parts

PartHrefineYour browser

What's wrong with it

checkUrl gives the standard's validation errors, each with where it is in what you typed, in code points: one, or a whole host or port. Browsers accept most of these URLs anyway; the last error, if parsing fails, says why.

    Other parsers

    Other languages' parsers can't run in your browser. For the gallery's URLs and the more disagreements below, this page shows their answers, recorded ahead of time. For anything else, only Hrefine and your browser run live.

    The trick gallery

    Each of these reads one way to a browser and another to some parser on a server. Differences like these are a known source of server-side request forgery (SSRF), open redirects, and allowlist bypasses, shown at length in Orange Tsai's well-known 2017 talk, “A New Era of SSRF,” on URL parsers in popular languages. Answers recorded 2026-10-30; see versions.

    same as the browser differs from the browser rejects it

    Short addresses

    http://127.1/

    Browsers read IPv4 addresses with missing parts, as the old inet_aton did, so 127.1 is 127.0.0.1. A parser that keeps the text lets it past a blocklist of 127.0.0.1, and the system's resolver usually connects to loopback anyway.

    4 parsers differ from the browser.

    Every parser's answer
    ParserHostComparedReads the URL as
    Browsers127.0.0.1http://127.0.0.1/
    Hrefine127.0.0.1 same as the browserhttp://127.0.0.1/
    Python urllib127.1 differs from the browserhttp://127.1/
    Java URIno host differs from the browserhttp://127.1/
    Java URL127.1 differs from the browserhttp://127.1/
    C# Uri127.0.0.1 same as the browserhttp://127.0.0.1/
    Go net/url127.1 differs from the browserhttp://127.1/
    curl127.0.0.1 same as the browserhttp://127.0.0.1/

    Hex addresses

    http://0x7f.1/

    Parts in hex (or octal, with a leading 0) are numbers too. A check that looks for internal addresses as text misses them; Hrefine says the host is an IPv4 address, whatever it looks like.

    4 parsers differ from the browser.

    Every parser's answer
    ParserHostComparedReads the URL as
    Browsers127.0.0.1http://127.0.0.1/
    Hrefine127.0.0.1 same as the browserhttp://127.0.0.1/
    Python urllib0x7f.1 differs from the browserhttp://0x7f.1/
    Java URIno host differs from the browserhttp://0x7f.1/
    Java URL0x7f.1 differs from the browserhttp://0x7f.1/
    C# Uri127.0.0.1 same as the browserhttp://127.0.0.1/
    Go net/url0x7f.1 differs from the browserhttp://0x7f.1/
    curl127.0.0.1 same as the browserhttp://127.0.0.1/

    One big number

    http://2130706433/

    A whole address as one decimal number, 127 × 2²⁴ + 1. Python, Java, and Go keep it as text, and the browser reads loopback.

    4 parsers differ from the browser.

    Every parser's answer
    ParserHostComparedReads the URL as
    Browsers127.0.0.1http://127.0.0.1/
    Hrefine127.0.0.1 same as the browserhttp://127.0.0.1/
    Python urllib2130706433 differs from the browserhttp://2130706433/
    Java URI2130706433 differs from the browserhttp://2130706433/
    Java URL2130706433 differs from the browserhttp://2130706433/
    C# Uri127.0.0.1 same as the browserhttp://127.0.0.1/
    Go net/url2130706433 differs from the browserhttp://2130706433/
    curl127.0.0.1 same as the browserhttp://127.0.0.1/

    Backslashes

    https:\\evil.example\path

    Browsers treat \ as / in http and https URLs. Python and Go find no host here at all, so a host check has nothing to stop, and the browser goes to evil.example.

    3 parsers differ from the browser, and 3 reject it.

    Every parser's answer
    ParserHostComparedReads the URL as
    Browsersevil.examplehttps://evil.example/path
    Hrefineevil.example same as the browserhttps://evil.example/path
    Python urllibno host differs from the browserhttps:///\\evil.example\path
    Java URIerror rejects itURISyntaxException: Illegal character in opaque part at index 6: https:\\evil.example\path
    Java URLempty host differs from the browserhttps:\\evil.example\path
    C# Urierror rejects itUriFormatException: Invalid URI: The hostname could not be parsed.
    Go net/urlno host differs from the browserhttps:\\evil.example\path
    curlerror rejects itUnsupported number of slashes following scheme

    Open redirects

    /\evil.example

    against https://good.example/

    A server checks that a next parameter is a path on its own site, and Python, Java's URL, C#, Go, and curl all say it is. The browser, resolving it against the page, goes to evil.example.

    5 parsers differ from the browser, and 1 rejects it.

    Every parser's answer
    ParserHostComparedReads the URL as
    Browsersevil.examplehttps://evil.example/
    Hrefineevil.example same as the browserhttps://evil.example/
    Python urllibgood.example differs from the browserhttps://good.example/\evil.example
    Java URIerror rejects itURISyntaxException: Illegal character in path at index 1: /\evil.example
    Java URLgood.example differs from the browserhttps://good.example/\evil.example
    C# Urigood.example differs from the browserhttps://good.example//evil.example
    Go net/urlgood.example differs from the browserhttps://good.example/%5Cevil.example
    curlgood.example differs from the browserhttps://good.example/\evil.example

    Who's the host?

    https://good.example@evil.example/

    Everything before @ is a user name, so the host is evil.example. Parsers agree; the trap is people, and checks like startsWith("https://good.example").

    Every parser here agrees. The trap is checking the text.

    Every parser's answer
    ParserHostComparedReads the URL as
    Browsersevil.examplehttps://good.example@evil.example/
    Hrefineevil.example same as the browserhttps://good.example@evil.example/
    Python urllibevil.example same as the browserhttps://good.example@evil.example/
    Java URIevil.example same as the browserhttps://good.example@evil.example/
    Java URLevil.example same as the browserhttps://good.example@evil.example/
    C# Urievil.example same as the browserhttps://good.example@evil.example/
    Go net/urlevil.example same as the browserhttps://good.example@evil.example/
    curlevil.example same as the browserhttps://good.example@evil.example/

    Fragments with @

    https://evil.example#@good.example/

    Everything after # is the fragment, so the host is evil.example. Parsers agree; a check that takes the text after the last @ reads good.example.

    Every parser here agrees. The trap is checking the text.

    Every parser's answer
    ParserHostComparedReads the URL as
    Browsersevil.examplehttps://evil.example/#@good.example/
    Hrefineevil.example same as the browserhttps://evil.example/#@good.example/
    Python urllibevil.example same as the browserhttps://evil.example#@good.example/
    Java URIevil.example same as the browserhttps://evil.example#@good.example/
    Java URLevil.example same as the browserhttps://evil.example#@good.example/
    C# Urievil.example same as the browserhttps://evil.example/#@good.example/
    Go net/urlevil.example same as the browserhttps://evil.example#@good.example/
    curlevil.example same as the browserhttps://evil.example/#@good.example/

    Tabs and newlines

    https://good.example\t.evil.example/

    Browsers drop tabs and newlines anywhere in a URL, so this host ends in .evil.example. Strict parsers refuse it, which is safe only if nothing downstream reads it the browser's way.

    5 reject it.

    Every parser's answer
    ParserHostComparedReads the URL as
    Browsersgood.example.evil.examplehttps://good.example.evil.example/
    Hrefinegood.example.evil.example same as the browserhttps://good.example.evil.example/
    Python urllibgood.example.evil.example same as the browserhttps://good.example.evil.example/
    Java URIerror rejects itURISyntaxException: Illegal character in authority at index 20: https://good.example .evil.example/
    Java URLerror rejects itMalformedURLException: Illegal character found in host: control char (code=9)
    C# Urierror rejects itUriFormatException: Invalid URI: The hostname could not be parsed.
    Go net/urlerror rejects itparse "https://good.example\t.evil.example/": net/url: invalid control character in URL
    curlerror rejects itMalformed input to a URL function

    javascript:, hidden

    java\tscript:alert(1)

    A filter looking for javascript: misses it, but browsers drop the tab and run the script. Check the parsed scheme, never the text.

    5 reject it.

    Every parser's answer
    ParserHostComparedReads the URL as
    Browsersno hostjavascript:alert(1)
    Hrefineno host same as the browserjavascript:alert(1)
    Python urllibno host same as the browserjavascript:alert(1)
    Java URIerror rejects itURISyntaxException: Illegal character in scheme name at index 4: java script:alert(1)
    Java URLerror rejects itMalformedURLException: no protocol: java script:alert(1)
    C# Urierror rejects itUriFormatException: Invalid URI: The URI scheme is not valid.
    Go net/urlerror rejects itparse "java\tscript:alert(1)": net/url: invalid control character in URL
    curlerror rejects itMalformed input to a URL function

    Look-alike letters

    https://аpple.com/

    The first letter is Cyrillic а, so browsers send xn--pple-43d.com, which isn't apple.com. Some parsers keep the Unicode, so a check and a request can see different names. This is the long-known IDN homograph trick.

    5 parsers differ from the browser.

    Every parser's answer
    ParserHostComparedReads the URL as
    Browsersxn--pple-43d.comhttps://xn--pple-43d.com/
    Hrefinexn--pple-43d.com same as the browserhttps://xn--pple-43d.com/
    Python urllibаpple.com differs from the browserhttps://аpple.com/
    Java URIno host differs from the browserhttps://аpple.com/
    Java URLаpple.com differs from the browserhttps://аpple.com/
    C# Urixn--pple-43d.com same as the browserhttps://аpple.com/
    Go net/urlаpple.com differs from the browserhttps://%D0%B0pple.com/
    curlаpple.com differs from the browserhttps://аpple.com/

    Look-alike hosts

    аррӏе.com shows as xn--80ak6aa92e.com

    Every letter is Cyrillic, but each looks Latin, so browsers show the xn-- form instead. Hrefine's display form decides as Chrome does, from Unicode's data.

    Other hosts, and why
    • аррӏе.comxn--80ak6aa92e.comkept as xn--All Cyrillic, but every letter looks Latin: a whole-script look-alike of apple.com.
    • аррӏе.рфаррӏе.рфshown in UnicodeThe same letters under .рф, a Cyrillic top-level domain, where readers expect Cyrillic.
    • аpple.comxn--pple-43d.comkept as xn--One Cyrillic а among Latin letters: mixed scripts.
    • пример.рфпример.рфshown in UnicodeRussian for “example”, with Cyrillic letters that don't look Latin.
    • 日本語.jp日本語.jpshown in UnicodeJapanese, in one script.

    Anyone can register names like these, so browsers check each label's letters against Unicode's data. Its scripts must go together: one script, or the mixes Chinese, Japanese, and Korean use. And a label whose every letter looks Latin keeps xn-- unless the top-level domain is in its own script. UrlDisplay.addressBar and domainToDisplay follow these rules. Show people that form, but decide, and hand on, with href, which is always ASCII.

    Dots that aren't dots

    https://good.example。evil.example/

    Browsers read the ideographic full stop 。 as a dot, so this is a name under evil.example. Parsers without IDNA see one odd label, and a check for names ending in .evil.example misses it.

    5 parsers differ from the browser.

    Every parser's answer
    ParserHostComparedReads the URL as
    Browsersgood.example.evil.examplehttps://good.example.evil.example/
    Hrefinegood.example.evil.example same as the browserhttps://good.example.evil.example/
    Python urllibgood.example。evil.example differs from the browserhttps://good.example。evil.example/
    Java URIno host differs from the browserhttps://good.example。evil.example/
    Java URLgood.example。evil.example differs from the browserhttps://good.example。evil.example/
    C# Urigood.example.evil.example same as the browserhttps://good.example。evil.example/
    Go net/urlgood.example。evil.example differs from the browserhttps://good.example%E3%80%82evil.example/
    curlgood.example。evil.example differs from the browserhttps://good.example。evil.example/

    Protocol-relative

    //evil.example/

    against https://good.example/

    It starts with /, so it looks like a path on your own site, but // starts a host. Every parser agrees; the trap is checks like startsWith("/").

    Every parser here agrees. The trap is checking the text.

    Every parser's answer
    ParserHostComparedReads the URL as
    Browsersevil.examplehttps://evil.example/
    Hrefineevil.example same as the browserhttps://evil.example/
    Python urllibevil.example same as the browserhttps://evil.example/
    Java URIevil.example same as the browserhttps://evil.example/
    Java URLevil.example same as the browserhttps://evil.example/
    C# Urievil.example same as the browserhttps://evil.example/
    Go net/urlevil.example same as the browserhttps://evil.example/
    curlevil.example same as the browserhttps://evil.example/

    Too many slashes

    https:///evil.example/

    Browsers and curl skip extra slashes and find evil.example. Python, Java, and Go see no host, and C# refuses it.

    4 parsers differ from the browser, and 1 rejects it.

    Every parser's answer
    ParserHostComparedReads the URL as
    Browsersevil.examplehttps://evil.example/
    Hrefineevil.example same as the browserhttps://evil.example/
    Python urllibno host differs from the browserhttps:///evil.example/
    Java URIno host differs from the browserhttps:///evil.example/
    Java URLempty host differs from the browserhttps:/evil.example/
    C# Urierror rejects itUriFormatException: Invalid URI: The hostname could not be parsed.
    Go net/urlempty host differs from the browserhttps:///evil.example/
    curlevil.example same as the browserhttps://evil.example/

    48 more disagreements

    Smaller tricks, by kind, many from web-platform-tests' cases and from published research on parser differences. Each mark says whether that parser reads the same host as browsers, or for paths, the same path. Pick an input to try it, with every parser's full answer.

    InputBrowsers readPython urllibJava URIJava URLC# UriGo net/urlcurl
    Backslashes, whitespace, and controls
    Browsers end the host at \; Python keeps it in the host.host good.example✗–––––
    against https://good.example/Two backslashes start a host, as // does.host evil.example✗–✗✓✗✗
    against https://good.example/The tab goes, leaving //evil.example: an open redirect past a check for //.host evil.example✓–✗✗––
    One slash and one backslash still start the host.host evil.example✗–✗✓✗–
    Browsers trim leading control characters, so this runs as javascript:.host no host✓–––––
    An escaped tab in a host is an error to browsers; Python and Java keep it as text.error✗✗✗✓✓✓
    A NUL in the host: curl stops reading at it, and finds good.example.error✗✓✓✓✓✗
    The tab goes, so ..\t is .., and the path is /b.path /b✗–✗✗––
    User info: @ and :
    The last @ ends the user info, so the host is evil.example.host evil.example✓✗✗–✓–
    An escaped @ decodes into the host, where it's forbidden.error✗✗✗✓✓✓
    From Orange Tsai's talk: three hosts to pick from. Browsers pick 2.2.2.2.host 2.2.2.2✓–✓✓––
    Looks like a host and port, but it's a user and password. Parsers agree; people don't.host evil.example✓✓✓✓✓✓
    IPv4 addresses
    A leading 0 means octal: 0177 is 127.host 127.0.0.1✗✗✗✓✗✓
    The whole address as one hex number.host 127.0.0.1✗✗✗✓✗✓
    Padded with zeros, so octal, and still loopback.host 127.0.0.1✗✗✗✓✗✓
    0.0.0.0, which reaches the local machine on many systems.host 0.0.0.0✗✗✗✓✗✓
    Browsers drop an address's trailing dot; others keep it, past a blocklist.host 127.0.0.1✗✗✗✗✗✗
    An escaped 127.1. curl decodes it but keeps the short form.host 127.0.0.1✗✗✗––✗
    One past the largest address: browsers refuse it, and every other parser takes it as a name.error✗✗✗✗✗✗
    Five numbers, which browsers refuse, since it ends in a number but isn't an address.error✗✗✗✗✗✗
    IPv6 addresses
    Zone IDs aren't in the standard, so browsers refuse them; curl drops the zone.error✗✗✗✗✗✗
    A dot after the brackets: only C# accepts it.error✓✓✓✗✓✓
    Loopback, as an IPv4-mapped IPv6 address. Parsers agree; a blocklist of IPv4 text misses it.host [::ffff:7f00:1]✓✓✓✓✓✓
    International and escaped names
    Browsers decode escapes in a host: this is evil.example.host evil.example✗✗✗––✓
    An escaped dot, decoded, splits the label.host evil.example✗✗✗––✓
    An escaped slash decodes into the host, where it's forbidden.error✗✗✗✓✓✓
    Full-width letters map to ASCII, by IDNA.host evil.example✗✗✗✓✗✗
    So do circled letters.host evil.example✗✗✗✓✗✗
    An invisible soft hyphen, which IDNA drops.host evil.example✗✗✗✓✗✗
    Today's IDNA keeps ß (xn--fa-hia), where the older IDNA 2003 made it fass.example, another name.host xn--fa-hia.example✗✗✗✓✗✗
    A trailing dot: the same name to DNS, but not to an allowlist that compares text.host evil.example.✓✓✓✓✓✓
    Schemes, slashes, and ports
    against https://good.example/Three slashes, and browsers still find a host.host evil.example✗✗✗–✗✓
    against https://good.example/A backslash and a slash start a host too.host evil.example✗–✗✓✗✗
    No slashes at all, and browsers still find a host.host evil.example✗✗✗–✗–
    against https://good.example/Against an https page, a new scheme without slashes still names a host.host evil.example✗✗✗–✗–
    against https://good.example/But with the page's own scheme, it's a path on the page's host.host good.example✓✗✓✓✗–
    One slash is enough for browsers.host evil.example✗✗✗–✗✓
    Ports stop at 65535; Python, Java, and Go take it.error✗✗✗✓✗✓
    Browsers refuse a hex port; Python and Java's URI don't.error✗✗✓✓✓✓
    A space before the port: Python ignores it.error✗✓✓✓✓✓
    Paths and percent-encoding
    Browsers read %2e%2e as .., so the path is /admin, past a check for /public/.path /admin✗✗✗✓✗✗
    Backslashes are slashes in browsers, so this is /admin too.path /admin✗–✗✓✗✗
    Dot segments resolve, and can't climb above the root.path /c✗✗✗✓✗✓
    An escaped slash isn't a slash to browsers, but C# decodes the dots.path /public/%2E%2E%2Fadmin✓✓✓✗✓✓
    Spaces are escaped as %20, not refused.path /a%20b✗–✗✓✓–
    Non-ASCII is escaped as UTF-8.path /%C3%A4✗✗✗✓✓✗
    A % without hex digits stays as it is; C# escapes it again.path /%zz✓–✓✗–✓
    Browsers leave | as it is, which RFC 3986 doesn't allow: Java's URI refuses it, even in href, and C# escapes it.path /a|b%5Ec✗–✗✗✗✗

    The fix: decide with Hrefine, hand on href

    Hrefine doesn't change how your HTTP client reads URLs. So check the URL Hrefine parsed, then pass on its href, never the original string:

    • Parse once, with Url.parse, which gives null for anything that isn't a URL.
    • Check the parsed parts: scheme, hostKind (a name, or an address however it's written), and hostname.
    • Or use a policy, built once: UrlPolicy.fetch, plus your allowlist, and its one check covers the scheme, credentials, private and loopback addresses however they're written, ports, a host allowlist, redirect targets, and strict parsing, and says which rule failed.
    • Hand on href, the standard's canonical form: no backslashes, tabs, or odd address forms left to disagree on. For clients that follow RFC 3986 strictly, such as Java's URI, hand on strictHref.
    • href has no credentials, by design, unlike JavaScript's URL.href: the username and password in the URL are left out, so handing it on or storing it can't leak them. Only to send them deliberately, use hrefWithCredentials(), and never log it. withoutCredentials() gives a Url without them.
    • Check redirects too, or turn them off. For a next parameter, resolve it against your own site and check sameOrigin.
    • Log toString(): it masks credentials as ***, so https://user:pass@host/ logs as https://***:***@host/, showing there were some. Tokens in a query or fragment aren't masked, so don't log URLs that may carry them.

    From the Python tour's “Using it safely” section, which CI runs in every language.

    Given href, or its strict form, the parsers agree

    The same parsers, given each of the 62 recorded URLs as typed, then given Hrefine's href for each URL Hrefine accepts, and then its strictHref: href with the few characters RFC 3986 doesn't allow (such as | and {) escaped, for strict parsers. Both are given with any credentials kept (hrefWithCredentials() and strictHrefWithCredentials()), so tricks in the user info are tested too. Counted is how often each reads the same host (or path, for path tricks) as the browser.

    ParserGiven the inputGiven Hrefine's hrefGiven its strictHref
    Python urllib16 of 6251 of 5151 of 51
    Java URI10 of 6249 of 51, 2 refused51 of 51
    Java URL14 of 6249 of 51, 2 refused49 of 51, 2 refused
    C# Uri36 of 6248 of 5151 of 51
    Go net/url17 of 6249 of 51, 1 refused51 of 51
    curl28 of 6249 of 51, 2 refused49 of 51, 2 refused

    Given href, the misses are paths: C# decodes escaped dots (%2E%2E%2F), and a % without hex digits (/%zz) and a |, which href keeps as they are, are refused by Java's URI, which follows RFC 3986, and by Go, or escaped again by C# and Go. strictHref escapes them, and decodes escaped dots, as RFC 3986's normalization does, so every parser agrees but Java's URL and curl, which refuse javascript:, a scheme they don't fetch, which is fine. Browsers read each strict form as they read its href, which the build checks.

    Same tests. Same answers. Every language.

    Hrefine is written once, in Temper, which turns it into a natural library for C++, C#, Java, JavaScript, Lua, Python, and Rust. Its tests are the ones browsers are checked against, web-platform-tests, and every case passes in all seven languages, in CI on every change:

    • 896 URLs, with and without bases, each with every part it must parse to, or a failure (urltestdata.json).
    • 2,760 of 2,762 international domain names (toascii.json and IdnaTestV2.json). The other 2 need unpaired surrogates, which Temper strings can't hold, so they're left out.

    The vendored tests, and where they come from.

    The URL tests, run in your browser

    The same 896 cases, run now in this page, through your browser's own URL and through Hrefine's JavaScript build. A case passes if it fails where it must, or gives the expected href and every part, as Hrefine's own test counts.

    • Your browser live Not run yet
    • Hrefine, in this page live Not run yet
    • Node 24.22.1 (Ada 4.0.0), at build time896 of 896

    They run once you scroll here, or now with this button.

    Browsers follow the standard closely, but some misses are usual: the standard keeps changing, browsers catch up over time, and some keep older behavior for compatibility, such as for file: URLs. Hrefine's point isn't a higher number, but the same answers in every language. Node's score is from when this site was built, by the same scoring code.

    The tour, in seven languages

    One program through the whole API, written in each language. Every version prints exactly the same text, and CI checks that they do.

    Run it:

    The recorded parsers

    Recorded 2026-10-30 on Linux x86_64, by site/record/record.py. Browsers follow the same standard as Node's URL, which stands in for them. Answers change between versions.

    • Browsers (Node's URL) (new URL(input, base)): Node v24.22.2
    • Hrefine: Hrefine's JavaScript build, in Node v24.22.2, and its Python build, which agrees
    • Python urllib (urllib.parse.urlsplit): Python 3.12.3
    • Java URI (java.net.URI): Eclipse Adoptium 21.0.4+7-LTS
    • Java URL (java.net.URL): Eclipse Adoptium 21.0.4+7-LTS
    • C# Uri (System.Uri): .NET 6.0.10
    • Go net/url (url.Parse): go1.21.0
    • curl (libcurl curl_url): libcurl 8.5.0

    The raw answers, with each parser's own errors: recorded.json.