This is an automated email from the ASF dual-hosted git repository.

HTHou pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/iotdb-client-nodejs.git


The following commit(s) were added to refs/heads/develop by this push:
     new a8ca4d2  Support IPv6 node URLs and filter wildcard redirect endpoints 
(#21)
a8ca4d2 is described below

commit a8ca4d21421b4db4ba8819ad810303973f7c52b2
Author: ZIHAN DAI <[email protected]>
AuthorDate: Fri Jul 24 20:09:47 2026 +1000

    Support IPv6 node URLs and filter wildcard redirect endpoints (#21)
    
    * Support IPv6 node URLs and filter wildcard redirect endpoints
    
    parseNodeUrls split each node URL on ":" and required exactly two parts, so
    an IPv6 endpoint such as "[::1]:6667" was rejected up front with "Invalid
    nodeUrl format", making the client unusable with IPv6 nodes.
    
    Parse the bracketed [ipv6]:port form (consistent with apache/iotdb#18162),
    keeping IPv4 and hostname parsing unchanged; a bare (unbracketed) IPv6
    address remains rejected as ambiguous. Also ignore a server redirect
    recommendation whose address is a wildcard/listen-all address (0.0.0.0 / 
::):
    the endpoint set from the server's redirectNode is consumed by
    Session.getAndClearLastRedirect() and then cached and connected to by the
    pool, so the guard is applied there. Add tests for both.
    
    Signed-off-by: Zihan Dai <[email protected]>
    
    * Reject node URLs with a non-numeric port suffix
    
    parseNodeUrl used parseInt, which accepts a numeric prefix, so
    "[::1]:6667junk" and "[::1]:6667:9999" were silently parsed as port
    6667. The port must now be an all-digits string before conversion.
    Adds regression tests for the bracketed, hostname and IPv4 forms.
    
    Addresses review feedback on #21.
    
    Signed-off-by: Zihan Dai <[email protected]>
    
    ---------
    
    Signed-off-by: Zihan Dai <[email protected]>
---
 src/client/Session.ts              |  7 ++++-
 src/utils/Config.ts                | 52 ++++++++++++++++++++++++++---------
 src/utils/Errors.ts                | 11 ++++++++
 tests/unit/Config.test.ts          | 31 +++++++++++++++++++--
 tests/unit/Errors.test.ts          | 34 +++++++++++++++++++++++
 tests/unit/SessionRedirect.test.ts | 55 ++++++++++++++++++++++++++++++++++++++
 6 files changed, 175 insertions(+), 15 deletions(-)

diff --git a/src/client/Session.ts b/src/client/Session.ts
index 73d180d..ef6424a 100644
--- a/src/client/Session.ts
+++ b/src/client/Session.ts
@@ -29,7 +29,7 @@ import { registerClosable, unregisterClosable } from 
"../utils/ProcessCleanup";
 import { SessionDataSet } from "./SessionDataSet";
 import { RowRecord } from "./RowRecord";
 import { BaseColumnDecoder, ColumnEncoding, Column } from "./ColumnDecoder";
-import { RedirectException } from "../utils/Errors";
+import { RedirectException, isWildcardAddress } from "../utils/Errors";
 import {
   serializeTabletValuesFast,
   serializeTimestamps
@@ -243,6 +243,11 @@ export class Session {
   getAndClearLastRedirect(): EndPoint | null {
     const redirect = this.lastRedirectEndpoint;
     this.lastRedirectEndpoint = null;
+    // Ignore a redirect to a wildcard/listen-all address (0.0.0.0 / ::),
+    // which is not a connectable remote endpoint (mirrors apache/iotdb#18162).
+    if (redirect && isWildcardAddress(redirect.host)) {
+      return null;
+    }
     return redirect;
   }
 
diff --git a/src/utils/Config.ts b/src/utils/Config.ts
index 05016f9..3c574b6 100644
--- a/src/utils/Config.ts
+++ b/src/utils/Config.ts
@@ -32,23 +32,51 @@ export interface InternalConfig extends Config {
   sqlDialect?: string;
 }
 
+/**
+ * Parse a single "host:port" node URL into an EndPoint. Accepts the bracketed
+ * IPv6 form "[::1]:6667" (consistent with the [ipv6]:port endpoint format
+ * standardized in apache/iotdb#18162) as well as IPv4 and hostname URLs. A 
bare
+ * (unbracketed) IPv6 address with a port is ambiguous and rejected; the
+ * "[ipv6]:port" form must be used.
+ */
+function parseNodeUrl(url: string): EndPoint {
+  const trimmed = url.trim();
+  let host: string;
+  let portStr: string;
+  if (trimmed.startsWith('[')) {
+    // Bracketed IPv6: [host]:port
+    const close = trimmed.indexOf(']');
+    if (close === -1 || trimmed[close + 1] !== ':') {
+      throw new Error(`Invalid nodeUrl format: ${url}. Expected format: 
"[ipv6]:port"`);
+    }
+    host = trimmed.slice(1, close).trim();
+    portStr = trimmed.slice(close + 2).trim();
+  } else {
+    const idx = trimmed.indexOf(':');
+    // A non-bracketed URL with more than one colon is a bare IPv6 address,
+    // which is ambiguous with a trailing port; it must be written as 
[ipv6]:port.
+    if (idx === -1 || idx !== trimmed.lastIndexOf(':')) {
+      throw new Error(`Invalid nodeUrl format: ${url}. Expected format: 
"host:port" (use "[ipv6]:port" for IPv6 addresses)`);
+    }
+    host = trimmed.slice(0, idx).trim();
+    portStr = trimmed.slice(idx + 1).trim();
+  }
+  // Require the port to be all digits: parseInt would otherwise accept a 
numeric prefix, so
+  // "[::1]:6667junk" or "[::1]:6667:9999" would be silently read as port 6667 
and mask a malformed
+  // endpoint (IoTDB's Java and Python parsers reject these because the whole 
value must convert).
+  const port = /^\d+$/.test(portStr) ? parseInt(portStr, 10) : NaN;
+  if (!host || isNaN(port) || port <= 0 || port > 65535) {
+    throw new Error(`Invalid nodeUrl format: ${url}. Host must be non-empty 
and port must be a valid number (1-65535)`);
+  }
+  return { host, port };
+}
+
 /**
  * Parse nodeUrls from string array format (e.g., ["host1:6667", "host2:6668"])
  * to EndPoint array format
  */
 export function parseNodeUrls(nodeUrls: string[]): EndPoint[] {
-  return nodeUrls.map((url) => {
-    const parts = url.split(':');
-    if (parts.length !== 2) {
-      throw new Error(`Invalid nodeUrl format: ${url}. Expected format: 
"host:port"`);
-    }
-    const host = parts[0].trim();
-    const port = parseInt(parts[1].trim(), 10);
-    if (!host || isNaN(port) || port <= 0 || port > 65535) {
-      throw new Error(`Invalid nodeUrl format: ${url}. Host must be non-empty 
and port must be a valid number (1-65535)`);
-    }
-    return { host, port };
-  });
+  return nodeUrls.map(parseNodeUrl);
 }
 
 export interface Config {
diff --git a/src/utils/Errors.ts b/src/utils/Errors.ts
index 0d3581c..ce89307 100644
--- a/src/utils/Errors.ts
+++ b/src/utils/Errors.ts
@@ -19,6 +19,17 @@
 
 import { EndPoint } from "./Config";
 
+/**
+ * A wildcard / listen-all address (IPv4 "0.0.0.0" or IPv6 "::") is not a
+ * connectable remote endpoint. When a server advertises one in a redirect
+ * recommendation, the client must ignore it and keep its current endpoint,
+ * consistent with apache/iotdb#18162.
+ */
+export function isWildcardAddress(host: string): boolean {
+  const h = host.trim().replace(/^\[/, "").replace(/\]$/, "");
+  return h === "0.0.0.0" || h === "::" || h === "0:0:0:0:0:0:0:0";
+}
+
 /**
  * Represents a redirect recommendation from the server.
  * Thrown when the server suggests a better endpoint for a device.
diff --git a/tests/unit/Config.test.ts b/tests/unit/Config.test.ts
index 1606f47..254aedd 100644
--- a/tests/unit/Config.test.ts
+++ b/tests/unit/Config.test.ts
@@ -182,11 +182,38 @@ describe('parseNodeUrls', () => {
 
   test('Should handle whitespace in nodeUrls', () => {
     const nodeUrls = [' localhost : 6667 '];
-    
+
     const parsed = parseNodeUrls(nodeUrls);
-    
+
     expect(parsed[0]).toEqual({ host: 'localhost', port: 6667 });
   });
+
+  test('Should parse bracketed IPv6 nodeUrls', () => {
+    const parsed = parseNodeUrls(['[::1]:6667', '[2001:db8::1]:6668']);
+
+    expect(parsed).toHaveLength(2);
+    expect(parsed[0]).toEqual({ host: '::1', port: 6667 });
+    expect(parsed[1]).toEqual({ host: '2001:db8::1', port: 6668 });
+  });
+
+  test('Should throw for a bare (unbracketed) IPv6 address', () => {
+    expect(() => parseNodeUrls(['::1:6667'])).toThrow('Invalid nodeUrl 
format');
+  });
+
+  test('Should throw for malformed bracketed IPv6', () => {
+    expect(() => parseNodeUrls(['[::1:6667'])).toThrow('Invalid nodeUrl 
format'); // unbalanced bracket
+    expect(() => parseNodeUrls(['[::1]6667'])).toThrow('Invalid nodeUrl 
format'); // missing colon before port
+    expect(() => parseNodeUrls(['[::1]:'])).toThrow('Invalid nodeUrl format'); 
// empty port
+  });
+
+  test('Should throw for a port with trailing non-digits', () => {
+    // parseInt accepts a numeric prefix; the whole port string must be digits 
so a malformed
+    // endpoint is rejected instead of silently parsed as the leading number.
+    expect(() => parseNodeUrls(['[::1]:6667junk'])).toThrow('Invalid nodeUrl 
format'); // trailing text
+    expect(() => parseNodeUrls(['[::1]:6667:9999'])).toThrow('Invalid nodeUrl 
format'); // extra :port
+    expect(() => parseNodeUrls(['localhost:6667junk'])).toThrow('Invalid 
nodeUrl format'); // hostname
+    expect(() => parseNodeUrls(['127.0.0.1:80x'])).toThrow('Invalid nodeUrl 
format'); // IPv4
+  });
 });
 
 describe('ConfigBuilder', () => {
diff --git a/tests/unit/Errors.test.ts b/tests/unit/Errors.test.ts
new file mode 100644
index 0000000..bdc36fc
--- /dev/null
+++ b/tests/unit/Errors.test.ts
@@ -0,0 +1,34 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { isWildcardAddress } from "../../src/utils/Errors";
+
+describe("isWildcardAddress", () => {
+  test("returns true for IPv4/IPv6 wildcard (listen-all) addresses", () => {
+    for (const h of ["0.0.0.0", "::", "0:0:0:0:0:0:0:0", "[::]", "[0.0.0.0]"]) 
{
+      expect(isWildcardAddress(h)).toBe(true);
+    }
+  });
+
+  test("returns false for real addresses", () => {
+    for (const h of ["127.0.0.1", "::1", "10.0.0.1", "example.com", "[::1]"]) {
+      expect(isWildcardAddress(h)).toBe(false);
+    }
+  });
+});
diff --git a/tests/unit/SessionRedirect.test.ts 
b/tests/unit/SessionRedirect.test.ts
new file mode 100644
index 0000000..a4e8152
--- /dev/null
+++ b/tests/unit/SessionRedirect.test.ts
@@ -0,0 +1,55 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { Session } from "../../src/client/Session";
+
+function newSession(): Session {
+  return new Session({
+    host: "localhost",
+    port: 6667,
+    username: "root",
+    password: "root",
+  });
+}
+
+// getAndClearLastRedirect is the single consumption point for the redirect
+// endpoint the server advertises (Session.lastRedirectEndpoint), before the
+// pool caches and connects to it — so the wildcard guard is exercised here.
+describe("Session.getAndClearLastRedirect", () => {
+  test("returns a normal redirect endpoint and clears it", () => {
+    const session = newSession();
+    (session as any).lastRedirectEndpoint = { host: "10.0.0.9", port: 6667 };
+
+    expect(session.getAndClearLastRedirect()).toEqual({
+      host: "10.0.0.9",
+      port: 6667,
+    });
+    // Cleared after reading.
+    expect(session.getAndClearLastRedirect()).toBeNull();
+  });
+
+  test("ignores a redirect to a wildcard/listen-all address", () => {
+    for (const host of ["0.0.0.0", "::"]) {
+      const session = newSession();
+      (session as any).lastRedirectEndpoint = { host, port: 6667 };
+
+      expect(session.getAndClearLastRedirect()).toBeNull();
+    }
+  });
+});

Reply via email to