fix: support IPv6 address parsing in upstream URL handling - #6456
fix: support IPv6 address parsing in upstream URL handling#6456wy471x wants to merge 19 commits into
Conversation
b626c8a to
c2c634a
Compare
There was a problem hiding this comment.
Pull request overview
Note
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Adds IPv6-safe upstream URL parsing to avoid breaking bracketed IPv6 host/port strings (e.g., [2001:db8::1]:8080) across health checks, discovery mapping, and service-doc loading.
Changes:
- Introduces a shared
parseHostPortutility (IPv6 bracket notation support) and wires it into call sites that previously usedsplit(":"). - Updates upstream URL checking to rely on
java.net.URIforhttp(s)://...inputs and the new host/port parser for barehost:portinputs. - Adds tests covering IPv6 parsing and IPv6 URL formatting/bracketing.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| shenyu-common/src/test/java/org/apache/shenyu/common/utils/UpstreamCheckUtilsTest.java | Adds regression tests ensuring IPv6 inputs don’t throw during URL checking. |
| shenyu-common/src/main/java/org/apache/shenyu/common/utils/UpstreamCheckUtils.java | Uses URI for http(s) URLs and delegates bare host:port parsing to IpUtils.parseHostPort. |
| shenyu-common/src/main/java/org/apache/shenyu/common/utils/IpUtils.java | Adds parseHostPort with IPv6 bracket notation handling and default port behavior. |
| shenyu-admin/src/test/java/org/apache/shenyu/admin/utils/CommonUpstreamUtilsTest.java | Adds tests for parsing host/port (IPv4/IPv6/hostname) and IPv6 bracketing in buildUrl. |
| shenyu-admin/src/main/java/org/apache/shenyu/admin/utils/CommonUpstreamUtils.java | Brackets IPv6 in buildUrl and delegates parseHostPort to IpUtils. |
| shenyu-admin/src/main/java/org/apache/shenyu/admin/transfer/DiscoveryTransfer.java | Replaces split(":") parsing with parseHostPort and simplifies upstream mapping. |
| shenyu-admin/src/main/java/org/apache/shenyu/admin/service/manager/impl/LoadServiceDocEntryImpl.java | Replaces split(":") parsing with parseHostPort when building UpstreamInstance. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
I found a compatibility issue in the IPv6 URL handling. This PR changes the canonical serialized IPv6 upstream URL from the old bare form ( That means an existing persisted IPv6 upstream written by older code, for example There are a few parser edge cases worth covering at the same time:
Could we add an upgrade-safe canonicalization strategy here, either by migrating old |
- IpUtils.parseHostPort: validate closing bracket for IPv6, default empty port to "80" - UpstreamCheckUtils.checkUrl: wrap non-http(s) parsing in try/catch to return false on bad input - DiscoveryTransfer: catch NumberFormatException and rethrow as IllegalArgumentException with context - CommonUpstreamUtils.buildUrl: avoid double-bracketing already-bracketed IPv6 hosts - Add edge case tests for missing ']' and empty port; use 1ms timeout in parsing-focused tests Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…module - Add IpUtils.parseHostPort validation for IPv6 bracket notation and IPv4 octet ranges, rejecting malformed inputs like [::1]junk and 192.111.111.555 - Add CommonUpstreamUtils.normalizeUrl to canonicalize URLs into a consistent format ([ipv6]:port or host:port) for storage and query - Normalize URLs before all select/insert/update/delete operations on discovery_upstream to ensure consistent matching regardless of input format (bracketed vs unbracketed IPv6, with/without default port) - Add host+port fallback matching as a shared utility to migrate old non-normalized records in-place during query operations - Add exception guards so a single malformed URL does not abort the entire batch event Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@Aias00 Hi, I have fixed the issues you mentioned. Please take a look when you have time. |
Aias00
left a comment
There was a problem hiding this comment.
Reviewed #6456 — IPv6 address parsing in upstream URL handling.
Thanks for this — the IPv6-aware parsing in IpUtils.parseHostPort and the URI-based checkUrl rewrite look correct (verified URI.getHost() returns the bracketed IPv6 form and InetSocketAddress accepts it). But there's a blocking issue plus a few consistency gaps to address before merge.
Blocker — CI is red (NPE regression in nativeCreateOrUpdate)
DiscoveryUpstreamServiceImpl.nativeCreateOrUpdate now calls CommonUpstreamUtils.normalizeUrl(...) unconditionally, and normalizeUrl → IpUtils.parseHostPort does upstreamUrl.startsWith("[") with no null guard. The existing DiscoveryUpstreamServiceTest.testNativeCreate builds a DTO with no URL set, so the PR now throws:
java.lang.NullPointerException: Cannot invoke "String.startsWith(String)" because "upstreamUrl" is null
at DiscoveryUpstreamServiceTest.testNativeCreate(DiscoveryUpstreamServiceTest.java:205)
at DiscoveryUpstreamServiceTest.testNativeCreateOrUpdate(DiscoveryUpstreamServiceTest.java:127)
This is the actual cause of the build (17, windows-latest) FAILURE (ci workflow); the ubuntu matrix was cancelled by fail-fast. mergeStateStatus is BLOCKED. Beyond the test, it's a runtime regression: master inserted the row (with null url) without throwing, the PR now 500s. The new DiscoveryUpstreamServiceImplTest always sets a URL via buildDTO, so it masks the regression; the pre-existing DiscoveryUpstreamServiceTest was not updated.
Fix options: null/blank-guard IpUtils.parseHostPort (or normalizeUrl), and add the same exception guard to the service path that you already added to the listener's handleAdded/Updated/Deleted. Then fix testNativeCreate to set a URL (or assert the new typed exception).
Should-fix
-
Listener swallows exceptions inside
@Transactional.handleUpdated/handleDeletedcatchException, log, and continue, butonChangeis@Transactional(rollbackFor=Exception.class)— so partial batches now commit instead of rolling back (master propagated → rollback). Either rethrow after logging to keep atomicity, or drop the@Transactionaland document per-item best-effort; don't swallow-and-commit silently. -
Service-layer write paths lack the host:port fallback.
deleteBySelectorIdAndUrlandchangeStatusBySelectorIdAndUrlonly exact-match the normalized URL; the listener additionally falls back tomatchByHostAndPortfor old-format records. So oldexample.com(no port) or unbracketed2001:db8::1:8080rows can't be deleted/status-toggled via the register/REST path until migrated, while the event path can. ReusematchByHostAndPortin both service methods (delete/update by id) so all write paths match. -
mapToCommonUpstreamis now throwable in unguarded streams. Replacingurl.split(":")[0]withparseHostPort(url)[0]means malformed bracketed input ([broken,[],[localhost]) throwsIllegalArgumentException. Callers inUpstreamCheckService(lines 482-484, 509) map with no try/catch — one bad row breaks the whole DIVIDE stream. Skip-and-log per item, or return null on parse failure and filter.
Nits
normalizeUrldoesInteger.parseInt(parts[1])butparseHostPortdoesn't validate the port token is numeric (example.com:abc→ uncaught NumberFormatException in the service path). Validate the port inparseHostPortor guard callers.- IPv6 coverage stops at admin/common.
ShenyuResolverHelper(grpc) andPrxInfoUtil(tars) stillsplit(":")and will NumberFormatException on[ipv6]:port. Pre-existing (unbracketed form was already broken), but please scope the PR description to admin/common, or open a follow-up for the gateway-side consumers.
Tests: the new IpUtilsTest / CommonUpstreamUtilsTest cases are good and cover IPv4/IPv6/hostname/bracket edge cases. Please also add round-trip normalizeUrl tests for hostname-without-port and IPv6-with-zone. And re-run CI with fail-fast: false so the ubuntu matrix actually executes.
…fallback, port validation, and defensive parse - Listener: rethrow after LOG.error in handleUpdated/handleDeleted so @transactional rollback works instead of silently committing partial batches. - Service layer: add matchByHostAndPort fallback in deleteBySelectorIdAndUrl and changeStatusBySelectorIdAndUrl so old-format records (missing port, unbracketed IPv6) can be deleted/status-toggled via the REST path, matching the listener path. - Transfer layer: catch IllegalArgumentException in mapToCommonUpstream and mapToDiscoveryUpstreamData; log and return null instead of breaking the entire stream. Callers in UpstreamCheckService filter null. - IpUtils.parseHostPort: validate port is numeric and in range 1–65535 at the parse boundary; prevents NumberFormatException from propagating through normalizeUrl. - Tests: add 11 round-trip normalizeUrl tests (hostname, IPv4, IPv6, IPv6 zone-id, plus error paths for non-numeric and out-of-range ports). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- IpUtils.parseHostPort: throw IllegalArgumentException for null/blank input instead of NPE - CommonUpstreamUtils.normalizeUrl: same guard for a clear error at the public API boundary - DiscoveryUpstreamServiceTest.testNativeCreate: set a URL on the DTO so the test exercises the actual create path Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Aias00
left a comment
There was a problem hiding this comment.
Follow-up after the new commits (d05fec8 + f0a26bf). Thanks — the substantive issues are addressed. Per-finding:
- #1 blocker (null-url NPE in
nativeCreateOrUpdate+ CI red): fixed.IpUtils.parseHostPortandCommonUpstreamUtils.normalizeUrlnow throwIllegalArgumentException("Invalid upstream URL, null or blank")on blank/null, andtestNativeCreatenow sets a URL. The originally-redk8s-examples-httpis passing again. Verified in commit f0a26bf. - #2 listener swallowed exceptions inside
@Transactional: fixed.handleUpdated/handleDeletednowthrow eafter logging (commit d05fec8), soonChangerolls back on failure — atomicity restored. - #3 service write paths lacked host:port fallback: fixed.
deleteBySelectorIdAndUrlandchangeStatusBySelectorIdAndUrlnow checkeffect == 0and fall back tomatchByHostAndPort+ delete/update by id, mirroring the listener path (d05fec8). - #4
mapToCommonUpstreamthrowable in unguarded streams: fixed.mapToCommonUpstreamandmapToDiscoveryUpstreamDatanow wrapparseHostPortin try/catch, returnnull+ log warn onIllegalArgumentException, and all three stream call sites (updateSelectorHandler,fetchUpstreamData,onDiscoveryUpstreamUpdated).filter(Objects::nonNull).mapToDiscoveryUpstreamData's null-url path now returnsnullinstead of throwing (d05fec8). - #5 non-numeric port
NumberFormatException: partially fixed. Blank/null is now a typedIllegalArgumentException. A non-numeric port (example.com:abc) still makesnormalizeUrl'sInteger.parseInt(parts[1])throwNumberFormatException. In the transfer/stream paths that's now harmless (NFE is a subclass of IAE, caught and skipped), but the directnormalizeUrlcall innativeCreateOrUpdatestill surfaces an untyped NFE → 500. Minor — consider validating the port token inparseHostPort(throw typed IAE) so the service path fails cleanly too. Not a blocker. - #6 IPv6 coverage stops at admin/common (
ShenyuResolverHelper/PrxInfoUtilstillsplit(":")): not addressed. Out of scope for this PR as flagged; please open a follow-up for the grpc/tars gateway-side consumers, or note in the PR description that this PR scopes to admin/common.
One thing to look at before merge: the build matrix is currently red on all OS jobs (windows + ubuntu 17/19/20/21), but it's not a code failure — the log shows ##[error]The operation was canceled. ~7 min in, with every surefire suite passing (Tests run: X, Failures: 0, Errors: 0) and no compile errors. It looks like a job timeout/cancellation, not a regression from these commits. Please re-run CI; if it reproduces, the build workflow's timeout-minutes may need bumping (the reactor is large and shared runners are slow). Once build is green I'm happy to approve — no code blockers remain.
Aias00
left a comment
There was a problem hiding this comment.
Correction to my previous comment — I was wrong that the build red was a timeout. I dug into the per-job conclusions of run 30756841941 and the real failure is on the Windows job; the four ubuntu jobs were cancelled by fail-fast (the build matrix uses the default fail-fast: true), which is why their logs end with The operation was canceled. mid-build. So the red is real test failures in this PR's own code, not an infra timeout. Please ignore my earlier "re-run and approve" line — re-running will fail the same way until these are fixed.
The Windows job (build (17, windows-latest)) ends with BUILD FAILURE — Tests run: 1474, Failures: 0, Errors: 3. The three errors:
-
DiscoveryUpstreamServiceTest.testNativeCreateOrUpdate:128 -> testNativeUpdate:200—java.lang.IllegalArgumentException: Invalid upstream URL, null or blank.
Yourf0a26bfblank-guard inparseHostPort/normalizeUrlthrows on a null/blank URL, and you fixedtestNativeCreateto set192.168.1.1:8080— buttestNativeUpdate(also called bytestNativeCreateOrUpdateat line 128) still builds a DTO with no URL set, so the new guard throws. Fix: set a URL intestNativeUpdatetoo (the blank-guard is the right behavior; the test was relying on the old null-tolerant path). -
CommonUpstreamUtilsTest.testNormalizeUrlIpv6WithZoneIdAndPortRoundTrip:506and
testNormalizeUrlIpv6WithZoneIdWithoutPortRoundTrip:515—java.lang.IllegalArgumentException: Invalid IPv6 address: fe80::1%lo0,Caused by: java.net.UnknownHostException: no such interface lo0.
These new tests usefe80::1%lo0, wherelo0is the macOS loopback interface name. Linux and Windows CI runners have nolo0interface, so the zone-id resolution fails and the test errors. So these tests are platform-dependent and break on every non-macOS runner (they'd fail on Linux too — windows just happened to reach the module first).
This points at a product gap, not just a test bug: parseHostPort resolves the IPv6 zone id to a real network interface, so on a Linux/Windows gateway an upstream URL containing a link-local IPv6 with a zone id (e.g. fe80::1%eth0) will fail to parse at runtime. For an IPv6-support PR, link-local + zone ids being unsupported on the production platform is a real correctness gap — please either parse the zone id without resolving it to an interface (treat it as an opaque scope id), or explicitly document that link-local zone-id upstreams are unsupported and skip these tests on CI rather than hardcoding a macOS interface name.
So: the blocker stands (CI red, 3 real failures). Keeping changes-requested. Once testNativeUpdate sets a URL and the zone-id case is either implemented without interface resolution or the tests are made platform-agnostic, build should go green.
Zone IDs (e.g., %lo0) are platform-specific interface names that cause UnknownHostException on non-macOS runners. Strip them before calling InetAddress.getByName() since they're irrelevant for address validation. Also add unit tests for non-numeric and empty port scenarios. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Hi,I have fixed the issues you mentioned. Please take a look when you have time. |
|
The core bracket-IPv6 path is correct, and the layered defense (skip malformed URLs in Non-bracketed IPv6 is silently misparsed.
One malformed URL aborts the whole sync batch. In Minor: Overall the direction is right; the three items above are the ones I'd want addressed before merge. |
Replace raw split(":") with IPv6-aware host:port parsing using bracket
notation detection and java.net.URI to avoid breaking IPv6 addresses
like [2001:db8::1]:8080 in health checks, discovery, and API doc loading.
Make sure that:
./mvnw clean install -Dmaven.javadoc.skip=true.Summary of All Changes
Problem
Four files parsed upstream URLs/host:port strings by splitting on :. This broke
IPv6 addresses like [2001:db8::1]:8080 — internal colons caused host truncation,
wrong port parsing, or NumberFormatException.
Files Changed
Core Logic — shenyu-common
IpUtils.java — New parseHostPort() method that correctly handles:
UpstreamCheckUtils.java — Adapted to use the new parseHostPort().
Wrapper & Callers — shenyu-admin
CommonUpstreamUtils.java — Two new utility methods:
DiscoveryTransfer.java — Uses normalizeUrl() when converting discovery upstream DTOs, plus null/blank guards on URL and properties.
DiscoveryUpstreamServiceImpl.java — Transaction atomicity (@transactional), host:port fallback logic, normalization on both create and update paths.
LoadServiceDocEntryImpl.java — Adapted to use new parseHostPort().
UpstreamCheckService.java — Adapted to use new parseHostPort().
DiscoveryDataChangedEventSyncListener.java — Null guard on discoveryUpstreams list.
Notes
Scope: shenyu-admin and shenyu-common only.
Follow-up: Gateway-side consumers (grpc, tars) to be handled in subsequent work.
close #6442