URL parsing in Node 24, Python 3.15 and Ruby 4.0

Dataset Last tested 24 October 2026

The current run accepted 37 of 40 fixtures in Node 24.22.1 LTS, 37 in Python 3.15.1 and 29 in Ruby 4.0.9 with uri 1.1.1. Compared with the earlier dated run, two Python observations changed; the recorded Node and Ruby values did not. These are parsing results, not safety verdicts.

What was measured

The matrix contains 120 local parsing observations: forty fixtures each passed through Node’s WHATWG URL API, Python’s urllib.parse and Ruby’s URI API. These are the newest stable Python and Ruby releases and the latest Node 24 LTS patch found on the checked release pages, not a promise that they remain current after the observation date.

Each fixture records its input, base and purpose. The adapters use new URL(input, base) in Node1, urljoin followed by urlsplit in Python2, and URI.join in Ruby3. The Python adapter also evaluates the port property, so an error at that step remains an error in the recorded observation.

The supplied base is part of the test. It lets the matrix include relative references, while the separate browser inspector intentionally requires an explicit scheme. Those two contracts should not be confused. Neither operation requests a destination.

Results in this run

Node accepted 37 of the forty fixtures, Python accepted 37 and Ruby accepted 29 under the declared adapters and versions. All three accepted 28 fixtures. Fifteen fixtures produced identical serialized strings across all three. These counts describe this deliberately varied fixture set, not a sample of ordinary browsing traffic.

IDPurposeNodePythonRubyEqual serialization
P01Userinfo and destination hostYYYN
P02Unicode and encoded path separatorYYN–
P03IPv6, explicit port and dot segmentsYYYN
P04Scheme-relative URL resolutionYYYN
P05Host case, default port and dot segmentsYYYN
P06Parent-relative referenceYYYY
P07Root-relative referenceYYYY
P08Query-only referenceYYYY
P09Fragment-only referenceYYYY
P10Empty relative referenceYYYY
P11Bare hostname-looking relative pathYYYY
P12HTTPS scheme without slashesYYYN
P13Backslash path separatorsYYN–
P14Encoded single-dot segmentYYYN
P15Encoded parent-dot segmentYYYN
P16Double-encoded dot textYYYY
P17Literal spaces in path and queryYYN–
P18Leading and trailing ASCII spacesYYN–
P19Embedded newline in hostnameYYN–
P20Embedded tab in hostnameYYN–
P21Encoded user-info delimiterYYYY
P22Synthetic username and passwordYYYY
P23Multiple at signsYYN–
P24Empty user informationYYYN
P25Default HTTP portYYYN
P26Nondefault HTTP portYYYY
P27Trailing-dot hostname and default portYYYN
P28ASCII punycode labelYYYY
P29Mixed-script subdomain; Cyrillic aYYN–
P30RFC5737 documentation IPv4YYYY
P31Integer spelling of documentation IPv4 192.0.2.1YYYN
P32Port zeroYYYY
P33Maximum unsigned 16-bit portYYYY
P34Out-of-range portNNY–
P35Negative port textNNN–
P36Invalid documentation IPv6 spellingNNN–
P37Empty explicit portYYYN
P38Repeated and empty query valuesYYYY
P39Percent-encoded ASCII host labelYYYN
P40Malformed percent escapesYYN–

Y means the adapter returned the recorded fields; N means it returned an error. Equal means all three accepted and their serialized strings matched. The full JSON and CSV retain the actual values and errors.

What changed from the earlier run

The earlier dated dataset remains separate. It used Node 24.19.0, Python 3.9.6 and Ruby 2.6.10; those measurements are not pooled into this table. All forty inputs and adapter operations were retained.

FixtureCurrent Python difference
P18Leading spaces no longer make the address resolve beneath the supplied base; trailing spaces remain in the serialized path.
P36Invalid IPv6 spelling now raises ValueError; the earlier adapter returned fields.

The change record preserves both complete outputs. Ruby issue 19266 documents the switch to an RFC3986 default parser.4 The executed runtime confirms that default and uri 1.1.1, yet none of this corpus’s forty URI.join observations changed. That is a limit of the observed comparison, not a claim that all old and new Ruby parsing behavior is equivalent.

Read differences at the field level

The first fixture uses user information before a destination hostname and an explicit default HTTPS port. All three adapters return the same destination host, but they do not preserve the serialized address identically. A port can be omitted from a serialization while a native API still exposes a default numeric value.

The Unicode fixture illustrates another distinction. Node serializes the internationalized hostname in ASCII form, Python preserves a Unicode representation in this fixture, and Ruby rejects the input under the recorded adapter. That result belongs to those APIs and versions. It is not a reason to call internationalized names invalid in every context.

The matrix keeps native nulls, strings and numbers instead of converting them into a common display value before comparison. This makes it possible to distinguish an omitted field from an empty string and a string port from a numeric port. Normalizing those differences for a particular application can be useful, but that would be a separate declared transformation.

Reproduce the observation

Download the fixtures, raw results and method. The portable runner is included with the dataset. It requires the declared interpreters; it does not install packages or fetch remote test inputs.

The method records isolated runtime installation, official archive checksums and two matching current runs. The repeat confirms reproducibility of the same 120 observations; it does not double the sample size. Setup failures are retained separately from completed runs.

Use the demo to find a fixture by ID or purpose. The table viewer filters the embedded records; it does not rerun the parsers. The JSON is the detailed observation record, while the compact article table is an index into it.

Reuse the compilation carefully

The original fixture compilation and observation data are CC BY 4.0. Runtime implementations retain their own licences and are not redistributed as this dataset. The source ledger links the API documentation used to describe each adapter.

When extending the matrix, preserve the original input, base, runtime version, adapter and error state. Add a new versioned result rather than overwriting an old cell with a newer runtime’s output. A change in serialization can then be examined as a change instead of disappearing into a revised table.

Not covered on this page

Sources

  1. Node 24.22.1 URL API. Opened 24 October 2026. ↩

  2. Python 3.15 URL parsing. Opened 24 October 2026. ↩

  3. Ruby 4.0 URI. Opened 24 October 2026. ↩

  4. Ruby issue 19266: change the default URI parser. Opened 24 October 2026. ↩

How we test: methodology. Found a mistake? Corrections.