Skip to content

fix(utils/ipaddr): render the unspecified address binary as "::" - #4998

Merged
yusukebe merged 1 commit into
honojs:mainfrom
sarathfrancis90:fix/ipaddr-unspecified-binary-to-string
Jun 3, 2026
Merged

yusukebe merged 1 commit into
honojs:mainfrom
sarathfrancis90:fix/ipaddr-unspecified-binary-to-string

Conversation

@sarathfrancis90

Copy link
Copy Markdown
Contributor

What this fixes

convertIPv6BinaryToString(0n) returns a single ":" instead of the canonical "::" for the IPv6 unspecified address (all-zero groups).

import { convertIPv6BinaryToString, convertIPv6ToBinary } from './src/utils/ipaddr'

convertIPv6BinaryToString(0n) // => ":"   (expected "::")

The resulting string is not a valid IPv6 address (net.isIP(':') is 0) and it does not round-trip: feeding it back into convertIPv6ToBinary throws Invalid IPv6 address: :.

Root cause

For the all-zero value every one of the 8 groups is "0", so the longest zero-run spans the whole address. The compression logic replaces that run with a single ":" placeholder via sections.splice(0, 8, ':'), leaving sections === [':']. Joining that with ":" yields ":", and the trailing value.replace(/:{2,}/, '::') cannot help because there is only one colon to collapse — the regex only matches runs of two or more colons.

The existing IPv4-mapped case is already handled with an early return at the top of the function; the all-zero case has the same "every group compresses" property and needs the same treatment. The recently merged RFC 5952 work (#4971 for single zero groups, #4973 for expanding ::) guards maxZeroEnd - maxZeroStart > 1, which still routes the all-zero value through the broken 8-group splice.

The fix

Add a 5-line early return for 0n in convertIPv6BinaryToString, mirroring the existing IPv4-mapped early return:

if (ipV6 === 0n) {
  return '::'
}

This also keeps the ip-restriction middleware's string rule map consistent: the unspecified address now canonicalizes to "::" rather than the bogus key ":", so a rule such as denyList: ['::'] is registered under the correct key.

Tests

Added a focused regression test asserting "::" output and a successful round-trip:

it('Should compress the unspecified address (all zero groups) to "::"', () => {
  expect(convertIPv6BinaryToString(0n)).toBe('::')
  expect(convertIPv6BinaryToString(convertIPv6ToBinary('::'))).toBe('::')
  expect(convertIPv6ToBinary(convertIPv6BinaryToString(0n))).toBe(0n)
})

Verified locally:

  • tsc --noEmit — exit 0
  • vitest run src/utils/ipaddr.test.ts — 41 passed
  • prettier --check on the touched files — clean
  • eslint on the touched files — clean

The author should do the following, if applicable

  • Add tests
  • Run tests
  • bun run format:fix && bun run lint:fix to format the code
  • Add TSDoc/JSDoc to document the code

`convertIPv6BinaryToString(0n)` returned a single ":" instead of the
canonical "::" for the IPv6 unspecified address. The all-zero address
compresses every group, so the generic zero-run logic collapsed the
sections array down to a lone ":" placeholder, which the trailing
`/:{2,}/` replacement could not fix because it only collapses runs of
two or more colons.

This produced an invalid IPv6 string (`net.isIP(':')` is `0`) and broke
the round-trip with `convertIPv6ToBinary`, which throws on ":". It also
meant the unspecified address was canonicalized to a bogus key in the
ip-restriction middleware's string rule map, so a rule such as
`denyList: ['::']` could be registered under ":" rather than "::".

Handle the all-zero value explicitly, mirroring the existing
IPv4-mapped early return.
@usualoma

usualoma commented Jun 3, 2026

Copy link
Copy Markdown
Member

@sarathfrancis90 Thank you!

@codecov

codecov Bot commented Jun 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 78.98%. Comparing base (5ef800e) to head (0071944).

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #4998   +/-   ##
=======================================
  Coverage   78.98%   78.98%           
=======================================
  Files         154      154           
  Lines       10568    10570    +2     
  Branches     2212     2213    +1     
=======================================
+ Hits         8347     8349    +2     
  Misses       2221     2221           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@yusukebe yusukebe left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@yusukebe

yusukebe commented Jun 3, 2026

Copy link
Copy Markdown
Member

@sarathfrancis90 Thanks! Merging.

@yusukebe
yusukebe merged commit c78932d into honojs:main Jun 3, 2026
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants