Why a selector?
rurl exposes many low-level normalization knobs
(path_encoding, path_normalization,
case_handling, the host model, …). Assembling a coherent
“RFC 3986-conformant” or “WHATWG-conformant” profile by hand means
getting several of them consistent at once, and getting one wrong yields
subtly non-conformant output.
The url_standard selector does that assembly for you. It
takes one of three values:
-
NULL(default) — today’s behavior, exactly. Fully backward compatible: byte-for-byte identical output and unchanged result shape. -
"rfc3986"— the RFC 3986 profile. -
"whatwg"— the WHATWG URL Standard profile (on the axesrurlgoverns).
There is no default flip: the selector is purely
additive, and NULL callers are unaffected. It is accepted
by safe_parse_url(), safe_parse_urls(), the
get_*() accessors, canonical_join(), and
resolve_url().
The canonical cases
Encoded unreserved bytes (%41%42)
%41%42 is the percent-encoding of the ASCII letters
AB — both unreserved bytes. RFC 3986 §6.2.2.2 says
unreserved percent-encodings should be decoded to their literal form;
WHATWG preserves them.
Encoded reserved bytes (%2F)
%2F is an encoded / — a reserved
byte. Neither standard decodes it into a path-separating slash (that
would change the path structure), so it stays encoded under both; only
the hex case is canonicalized.
Whole-number IPv4 (2130706433)
2130706433 is the 32-bit integer form of
127.0.0.1. WHATWG coerces it to a dotted-decimal address;
RFC 3986 has no such rule, so it is treated as a registered name — but a
diagnostic fires under both standards so a caller can
tell the host was a numeric shorthand.
get_host("http://2130706433/", url_standard = "whatwg")
#> [1] "127.0.0.1"
get_host_type("http://2130706433/", url_standard = "whatwg")
#> [1] "ipv4"
get_url_diagnostics("http://2130706433/", url_standard = "rfc3986")
#> [1] "ipv4-number-form" "ipv4-non-dotted"Diagnostics are facts, not policy
url_standard also unlocks three companion helpers. They
return NA unless you pass a selector, and — importantly —
they never widen the parse result shape: metadata is surfaced only
through these helpers, never as new columns or fields.
get_host_type("http://example.com/", url_standard = "whatwg")
#> [1] "domain"
get_scheme_class(c("http://a/", "ftps://a/"), url_standard = "whatwg")
#> [1] "special" "non-special"
get_url_diagnostics("http://0x7f.1/", url_standard = "whatwg")
#> [1] "ipv4-short-form" "ipv4-non-decimal"A diagnostic describes a shape of the input; it does not
decide what to do about it. A link-graph builder can ignore
ipv4-* tokens when computing keys, while an SSRF/allowlist
guard can reject any host that carries one. Because they are facts, the
same token fires under both standards.
Ports and backslashes
port_handling controls whether the port appears in
clean_url. It is a standalone editorial knob (independent
of url_standard), but under "whatwg" its
"keep" value elides a port that matches its special
scheme’s default.
get_clean_url("http://example.com:80/p", port_handling = "keep")
#> [1] "http://example.com:80/p"
get_clean_url("http://example.com:80/p", port_handling = "keep",
url_standard = "whatwg")
#> [1] "http://example.com:80/p"
get_clean_url("http://example.com:8080/p", port_handling = "keep",
url_standard = "whatwg")
#> [1] "http://example.com:8080/p"Under "whatwg", a literal backslash is recognized as a
path separator for the WHATWG-special schemes
(http/https/ftp), as browsers do.
%5C is never treated as a separator, and
"rfc3986" leaves backslashes inert.
get_clean_url("http://example.com/a\\b", url_standard = "whatwg")
#> [1] "http://example.com/a/b"
get_clean_url("http://example.com/a\\b", url_standard = "rfc3986")
#> [1] NAResolving references
resolve_url() implements RFC 3986 §5 reference
resolution — turning a relative link and a base URL into an absolute URL
— and then canonicalizes the result with the same machinery, so
url_standard and the other options flow straight
through.
resolve_url("../g", "http://a/b/c/d;p?q")
#> [1] "http://a/b/g"
resolve_url("//other.example/p", "http://a/b/c")
#> [1] "http://other.example/p"
resolve_url(c("g", "../h"), "http://a/b/c/")
#> [1] "http://a/b/c/g" "http://a/b/h"Migration notes
-
Pin
url_standard = "whatwg"for WHATWG-aligned link identity on the governed axes (path percent/dot handling, the host IPv4/reg-name model). This collapses a hand-tuned multi-knob profile down to one argument. It is deliberately narrower than “browser-faithful”: IDNA rendering and query handling are not governed by the selector. -
path_encoding = "keep"is a stopgap, not a profile. If you adoptedpath_encoding = "keep"earlier to stop%2Freserved-byte false joins, keep in mind it is a collision fix, not behavior-equivalent to"whatwg": it leaves%41%42encoded but does not resolve encoded dot segments and does not change host numeric parsing. Prefer the selector once you can.
What the selector does not govern
The selector governs path percent/dot handling, the host
IPv4/reg-name model, case_handling, default-port elision
(under port_handling = "keep"), and WHATWG backslash
recognition. It does not govern IDNA rendering
(host_encoding) or query handling (owned by the query
options).
It also does not decide which schemes get parsed at
all — that is a separate axis, scheme_acceptance.
The default "web" admits only the curated
http/https/ftp/ftps/file
allowlist; "general" additionally accepts non-special
schemes, including opaque-path ones such as mailto:,
data: and tel:. The two axes compose:
scheme_acceptance decides what gets parsed,
url_standard decides how the result is interpreted.
# Rejected by the default "web" allowlist ...
get_host("mailto:jane@example.com", url_standard = "whatwg")
#> [1] NA
# ... parsed under "general".
get_host("mailto:jane@example.com", url_standard = "whatwg",
scheme_acceptance = "general")
#> [1] "example.com"A mailto: URL is a WHATWG opaque path,
which by definition has no authority, so the parse result reports none —
the recipient stays in path. The accessor above is doing
something different and deliberate: extracting the web-y facts
about an address. The two answers are allowed to differ.
safe_parse_urls("mailto:jane@example.com", url_standard = "whatwg",
scheme_acceptance = "general")[, c("host", "user", "path")]
#> host user path
#> 1 <NA> <NA> jane@example.comConformance posture
rurl’s conformance claims are measured, frozen and
regenerable — the studies live in analysis/,
with the harnesses in inst/bench/.
WHATWG. Against the spec’s own conformance suite
(web-platform-tests urltestdata.json), at
scheme_acceptance = "general" the "whatwg"
profile accepts all 336 success cases with full
component parity — scheme, username, password, host, port, path, query
and fragment — and correctly rejects all 202 failure
cases: 538/538. At the default
scheme_acceptance = "web" the same corpus scores 176
accepted, all at full parity, plus the same 202 rejections; the 160
difference is the ADR 0004 allowlist declining non-web schemes before
the grammar is consulted, not a conformance miss. Against
adaR (the Ada C++ WHATWG reference) over a 336-input
adversarial corpus, it differs on two rows, and neither
is a parsing disagreement: one is punycode-vs-Unicode
presentation (ADR 0002 — reachable through
host_encoding), and one is the held
scheme_policy = "require" axis. On the rows where both
parsers accept, scheme, port and path agreement are all 1.000.
RFC 3986. Over the 257-row audited conformance
fixture, the "rfc3986" profile matches the standard on
164 rows and departs on 93 — 81 where
it rejects what the RFC admits (all of them the ADR 0004 host/authority
gate; the closed scheme set contributes none) and 12 where it accepts
what the RFC does not (ADR 0002 Unicode hosts, ADR 0011 readable paths).
Every one of the 93 cites the ADR or ticket that owns it; none is
accidental. A hand-authored, two-sided probe set covering the grammar
and normalization rules passes 32/32 in both directions, with five
deliberate departures pinned outside the score so strictness
cannot inflate a conformance number.
Two boundaries keep that honest rather than triumphal:
- The WPT fixture keeps only absolute URLs (rows with
a null base), because rurl does not resolve relative references against
a base URL. 336/336 says nothing about base-relative resolution. It does
now cover every scheme the suite exercises, including opaque,
ws:andwss:. - The
"whatwg"profile is WHATWG on the axes it governs — not a full UTS-46 host mapping. Ligatures, circled digits and zero-width code points pass through unmapped (ADR 0002).
The residual WHATWG deviations are both dials rather than defects: each is reachable from a documented argument, which is the design’s whole claim.