Skip to content

fix(rlhf): percent-encode the IPv6 zone id in rollout base URLs - #10200

Open
Redemption-ZTX wants to merge 1 commit into
modelscope:mainfrom
Redemption-ZTX:fix/ipv6-zone-id-url
Open

Redemption-ZTX wants to merge 1 commit into
modelscope:mainfrom
Redemption-ZTX:fix/ipv6-zone-id-url

Conversation

@Redemption-ZTX

@Redemption-ZTX Redemption-ZTX commented Sep 19, 2026

Copy link
Copy Markdown

What

format_host_for_url wraps IPv6 addresses in brackets but copies the address verbatim, so a scoped address produces a URL with an unencoded zone id:

format_host_for_url('fe80::1%eth0')  # -> '[fe80::1%eth0]'   (before)
                                     # -> '[fe80::1%25eth0]'  (after)

RFC 6874 requires the zone id to be percent-encoded inside the brackets. Left unencoded, the URL carries %et, which HTTP clients reject as a malformed percent-escape.

Where it matters

format_host_for_url has a single call site, building the rollout base URLs from the caller-supplied hosts:

self.base_urls = [f'http://{format_host_for_url(h)}:{p}'
                  for h, p in zip(hosts, server_ports)]   # vllm_client.py:100

Any scoped (link-local) IPv6 reaching that list — e.g. from a config that addresses a node by its link-local address — yields a URL that HTTP clients reject.

Correction: an earlier version of this description claimed the address comes from resolve_hostname() (vllm_client.py:91). That is wrong: resolve_hostname() sits in the other branch of the same if/else, and its result only feeds self.hosts (used for bind_host and the weight-sync connection at :239 and :253), never format_host_for_url. The helper itself is still wrong on scoped input, which is what this PR fixes, but the reachability is narrower than I first wrote.

Change

Only the single % separator is encoded (host.partition('%')), so a raw scoped address is encoded exactly once. Addresses without a zone id are unaffected, and ipaddress.IPv6Address rejects the bracketed form, so an already-wrapped host is not wrapped twice.

resolve_hostname and is_valid_ipv6_address are left untouched.

Tests

Adds tests/general/test_ipv6_host_format.py (9 cases): IPv4 and hostname pass-through, plain IPv6 bracketing, zone-id encoding, the already-bracketed form, and that the built URL still parses into the expected port and path.

python -m pytest tests/general/test_ipv6_host_format.py -q
  • 9 passed with the fix
  • Verified the tests catch the bug: reverting format_host_for_url to the original body fails test_scoped_ipv6_zone_is_percent_encoded
  • pre-commit clean (flake8, isort, yapf)

No GPU required — this is pure ipaddress / urllib.parse logic.

`format_host_for_url` wraps IPv6 addresses in brackets but copies the address
verbatim, so a scoped address such as `fe80::1%eth0` produced
`http://[fe80::1%eth0]:8000`. RFC 6874 requires the zone id to be
percent-encoded inside the brackets (`%25`); otherwise the URL carries `%et`,
which HTTP clients reject as a malformed percent-escape.

This is reachable because `vllm_client.py` resolves hostnames with
`resolve_hostname()` -- which supports IPv6 and can return a link-local
address with a zone id -- and then builds base URLs from the result:

    self.base_urls = [f'http://{format_host_for_url(h)}:{p}'
                      for h, p in zip(hosts, server_ports)]

Only the single `%` separator is encoded, so a raw scoped address from
`resolve_hostname` is encoded exactly once.

Adds tests/general/test_ipv6_host_format.py covering IPv4/hostname
pass-through, plain IPv6 bracketing, zone-id encoding, the already-bracketed
form, and that the built URL still parses. The zone-id case fails without the
fix.
@tastelikefeet tastelikefeet added invalid This doesn't seem right pending labels Sep 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

invalid This doesn't seem right pending

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants