From 82f012c353479198c8c809cb4f718a401e579bca Mon Sep 17 00:00:00 2001 From: Kentaro Hayashi Date: Tue, 10 Feb 2026 10:26:17 +0900 Subject: [PATCH 1/8] fix: Support IPv6 bind addresses in http_server helper Fixes URI::InvalidURIError when binding to IPv6 addresses like '::' or '::1'. The http_server helper was generating invalid URIs such as 'http://:::24231' instead of the RFC 3986 compliant 'http://[::]:24231'. Changes: - Wrap IPv6 addresses in brackets when constructing URIs - Add unit tests for IPv6 localhost (::1) and wildcard (::) binding - Update CHANGELOG.md This bug affected all Fluentd versions including v1.19.1 and caused crashes when attempting to bind HTTP servers to IPv6 addresses in dual-stack environments. Resolves: Invalid URI generation for IPv6 bind addresses Signed-off-by: Jesse Awan Signed-off-by: Kentaro Hayashi --- CHANGELOG.md | 11 ++++++ .../plugin_helper/http_server/server.rb | 3 +- test/plugin_helper/test_http_server_helper.rb | 37 +++++++++++++++++++ 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f6d8a2b02e..3ff00fb4ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +# v1.20 + +## Release v1.20.0 - TBD + +### Bug fix + +* http_server helper: Fix IPv6 bind address support in URI construction + * Fixed `URI::InvalidURIError` when binding to IPv6 addresses (e.g., `::`) + * IPv6 addresses are now properly bracketed in URIs per RFC 3986 (e.g., `http://[::]:24231`) + * Affects all plugins using http_server helper with IPv6 bind addresses + # v1.19 ## Release v1.19.1 - 2025/11/06 diff --git a/lib/fluent/plugin_helper/http_server/server.rb b/lib/fluent/plugin_helper/http_server/server.rb index 9c099292d8..85ae74b58a 100644 --- a/lib/fluent/plugin_helper/http_server/server.rb +++ b/lib/fluent/plugin_helper/http_server/server.rb @@ -37,7 +37,8 @@ def initialize(addr:, port:, logger:, default_app: nil, tls_context: nil) # TODO: support http2 scheme = tls_context ? 'https' : 'http' - @uri = URI("#{scheme}://#{@addr}:#{@port}").to_s + addr_display = @addr.include?(":") ? "[#{@addr}]" : @addr + @uri = URI("#{scheme}://#{addr_display}:#{@port}").to_s @router = Router.new(default_app) @server_task = nil Console.logger = Fluent::Log::ConsoleAdapter.wrap(@logger) diff --git a/test/plugin_helper/test_http_server_helper.rb b/test/plugin_helper/test_http_server_helper.rb index bc9853c013..a101714c4c 100644 --- a/test/plugin_helper/test_http_server_helper.rb +++ b/test/plugin_helper/test_http_server_helper.rb @@ -21,6 +21,16 @@ def teardown @port = nil end + def ipv6_enabled? + begin + sock = Socket.new(Socket::AF_INET6, Socket::SOCK_STREAM, 0) + sock.close + true + rescue + false + end + end + class Dummy < Fluent::Plugin::TestBase helpers :http_server end @@ -364,6 +374,33 @@ def start_https_request(addr, port, verify: true, cert_path: nil, selfsigned: tr end end + test 'bind to IPv6 address' do + omit('IPv6 not supported') unless ipv6_enabled? + on_driver do |driver| + driver.http_server_create_http_server(:http_server_helper_test, addr: '::1', port: @port, logger: NULL_LOGGER) do |s| + s.get('/example/hello') { [200, { 'Content-Type' => 'text/plain' }, 'hello from ipv6'] } + end + + resp = get("http://[::1]:#{@port}/example/hello") + assert_equal('200', resp.code) + assert_equal('hello from ipv6', resp.body) + end + end + + test 'bind to IPv6 wildcard address' do + omit('IPv6 not supported') unless ipv6_enabled? + on_driver do |driver| + driver.http_server_create_http_server(:http_server_helper_test, addr: '::', port: @port, logger: NULL_LOGGER) do |s| + s.get('/example/hello') { [200, { 'Content-Type' => 'text/plain' }, 'hello from ipv6 wildcard'] } + end + + # Can access via IPv4-mapped IPv6 or IPv6 loopback + resp = get("http://[::1]:#{@port}/example/hello") + assert_equal('200', resp.code) + assert_equal('hello from ipv6 wildcard', resp.body) + end + end + test 'must be called #start and #stop' do on_driver do |driver| server = flexmock('Server') do |watcher| From bf9e2390bb5b4f542f9ab2e98a401fe173b7a8e6 Mon Sep 17 00:00:00 2001 From: Jesse Awan Date: Wed, 28 Jan 2026 10:46:56 +0100 Subject: [PATCH 2/8] fix: Handle pre-bracketed IPv6 addresses to avoid double-bracketing - Updated server.rb to check if IPv6 addresses are already bracketed - Added tests for pre-bracketed addresses ([::1] and [::]) - Improved ipv6_enabled? helper to verify both binding and resolution - Updated CHANGELOG to document pre-bracketed address handling Signed-off-by: Jesse Awan --- CHANGELOG.md | 3 +- .../plugin_helper/http_server/server.rb | 10 +++++- test/plugin_helper/test_http_server_helper.rb | 36 +++++++++++++++++-- 3 files changed, 44 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ff00fb4ee..659da5c06f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,9 @@ ### Bug fix * http_server helper: Fix IPv6 bind address support in URI construction - * Fixed `URI::InvalidURIError` when binding to IPv6 addresses (e.g., `::`) + * Fixed `URI::InvalidURIError` when binding to IPv6 addresses (e.g., `::`, `::1`) * IPv6 addresses are now properly bracketed in URIs per RFC 3986 (e.g., `http://[::]:24231`) + * Handles pre-bracketed addresses correctly to avoid double-bracketing * Affects all plugins using http_server helper with IPv6 bind addresses # v1.19 diff --git a/lib/fluent/plugin_helper/http_server/server.rb b/lib/fluent/plugin_helper/http_server/server.rb index 85ae74b58a..b59450c242 100644 --- a/lib/fluent/plugin_helper/http_server/server.rb +++ b/lib/fluent/plugin_helper/http_server/server.rb @@ -37,7 +37,15 @@ def initialize(addr:, port:, logger:, default_app: nil, tls_context: nil) # TODO: support http2 scheme = tls_context ? 'https' : 'http' - addr_display = @addr.include?(":") ? "[#{@addr}]" : @addr + # Handle IPv6 addresses properly in URI construction per RFC 3986 + # Check if address is already bracketed to avoid double-bracketing + addr_display = if @addr.include?(":") && !@addr.start_with?('[') + "[#{@addr}]" # Bare IPv6 address - add brackets + elsif @addr.start_with?('[') && @addr.end_with?(']') + @addr # Already bracketed - use as-is + else + @addr # IPv4 or hostname - use directly + end @uri = URI("#{scheme}://#{addr_display}:#{@port}").to_s @router = Router.new(default_app) @server_task = nil diff --git a/test/plugin_helper/test_http_server_helper.rb b/test/plugin_helper/test_http_server_helper.rb index a101714c4c..f71400ded3 100644 --- a/test/plugin_helper/test_http_server_helper.rb +++ b/test/plugin_helper/test_http_server_helper.rb @@ -23,10 +23,16 @@ def teardown def ipv6_enabled? begin + # Try to actually bind to an IPv6 address to verify it works sock = Socket.new(Socket::AF_INET6, Socket::SOCK_STREAM, 0) + sock.bind(Socket.sockaddr_in(0, '::1')) sock.close + + # Also test that we can resolve IPv6 addresses + # This is needed because some systems can bind but can't connect + Socket.getaddrinfo('::1', nil, Socket::AF_INET6) true - rescue + rescue Errno::EADDRNOTAVAIL, Errno::EAFNOSUPPORT, SocketError false end end @@ -380,7 +386,6 @@ def start_https_request(addr, port, verify: true, cert_path: nil, selfsigned: tr driver.http_server_create_http_server(:http_server_helper_test, addr: '::1', port: @port, logger: NULL_LOGGER) do |s| s.get('/example/hello') { [200, { 'Content-Type' => 'text/plain' }, 'hello from ipv6'] } end - resp = get("http://[::1]:#{@port}/example/hello") assert_equal('200', resp.code) assert_equal('hello from ipv6', resp.body) @@ -393,7 +398,6 @@ def start_https_request(addr, port, verify: true, cert_path: nil, selfsigned: tr driver.http_server_create_http_server(:http_server_helper_test, addr: '::', port: @port, logger: NULL_LOGGER) do |s| s.get('/example/hello') { [200, { 'Content-Type' => 'text/plain' }, 'hello from ipv6 wildcard'] } end - # Can access via IPv4-mapped IPv6 or IPv6 loopback resp = get("http://[::1]:#{@port}/example/hello") assert_equal('200', resp.code) @@ -401,6 +405,32 @@ def start_https_request(addr, port, verify: true, cert_path: nil, selfsigned: tr end end + test 'handle already bracketed IPv6 address' do + omit('IPv6 not supported') unless ipv6_enabled? + on_driver do |driver| + # Test that pre-bracketed addresses don't get double-bracketed + driver.http_server_create_http_server(:http_server_helper_test, addr: '[::1]', port: @port, logger: NULL_LOGGER) do |s| + s.get('/example/hello') { [200, { 'Content-Type' => 'text/plain' }, 'hello from bracketed ipv6'] } + end + resp = get("http://[::1]:#{@port}/example/hello") + assert_equal('200', resp.code) + assert_equal('hello from bracketed ipv6', resp.body) + end + end + + test 'handle already bracketed IPv6 wildcard address' do + omit('IPv6 not supported') unless ipv6_enabled? + on_driver do |driver| + driver.http_server_create_http_server(:http_server_helper_test, addr: '[::]', port: @port, logger: NULL_LOGGER) do |s| + s.get('/example/hello') { [200, { 'Content-Type' => 'text/plain' }, 'hello from bracketed ipv6 wildcard'] } + end + # Access via IPv6 loopback + resp = get("http://[::1]:#{@port}/example/hello") + assert_equal('200', resp.code) + assert_equal('hello from bracketed ipv6 wildcard', resp.body) + end + end + test 'must be called #start and #stop' do on_driver do |driver| server = flexmock('Server') do |watcher| From 2171cc80144945841a9748da0c8a74bfa499af33 Mon Sep 17 00:00:00 2001 From: Jesse Awan Date: Tue, 3 Feb 2026 11:43:44 +0100 Subject: [PATCH 3/8] fix: Normalize IPv6 addresses for socket binding Signed-off-by: Jesse Awan --- .../plugin_helper/http_server/server.rb | 16 +- test/plugin_helper/test_http_server_helper.rb | 146 ++++++++++++------ 2 files changed, 113 insertions(+), 49 deletions(-) diff --git a/lib/fluent/plugin_helper/http_server/server.rb b/lib/fluent/plugin_helper/http_server/server.rb index b59450c242..d68939b40a 100644 --- a/lib/fluent/plugin_helper/http_server/server.rb +++ b/lib/fluent/plugin_helper/http_server/server.rb @@ -31,18 +31,22 @@ class Server # @param default_app [Object] This method must have #call. # @param tls_context [OpenSSL::SSL::SSLContext] def initialize(addr:, port:, logger:, default_app: nil, tls_context: nil) - @addr = addr + # Normalize the address: strip brackets if present + # Brackets are only for URI formatting, not for socket binding + @addr = if addr.start_with?('[') && addr.end_with?(']') + addr[1..-2] # Remove surrounding brackets + else + addr + end @port = port @logger = logger # TODO: support http2 scheme = tls_context ? 'https' : 'http' # Handle IPv6 addresses properly in URI construction per RFC 3986 - # Check if address is already bracketed to avoid double-bracketing - addr_display = if @addr.include?(":") && !@addr.start_with?('[') - "[#{@addr}]" # Bare IPv6 address - add brackets - elsif @addr.start_with?('[') && @addr.end_with?(']') - @addr # Already bracketed - use as-is + # Add brackets to IPv6 addresses for URI compliance + addr_display = if @addr.include?(":") + "[#{@addr}]" # IPv6 address - add brackets else @addr # IPv4 or hostname - use directly end diff --git a/test/plugin_helper/test_http_server_helper.rb b/test/plugin_helper/test_http_server_helper.rb index f71400ded3..35d57f17c0 100644 --- a/test/plugin_helper/test_http_server_helper.rb +++ b/test/plugin_helper/test_http_server_helper.rb @@ -380,55 +380,115 @@ def start_https_request(addr, port, verify: true, cert_path: nil, selfsigned: tr end end - test 'bind to IPv6 address' do - omit('IPv6 not supported') unless ipv6_enabled? - on_driver do |driver| - driver.http_server_create_http_server(:http_server_helper_test, addr: '::1', port: @port, logger: NULL_LOGGER) do |s| - s.get('/example/hello') { [200, { 'Content-Type' => 'text/plain' }, 'hello from ipv6'] } - end - resp = get("http://[::1]:#{@port}/example/hello") - assert_equal('200', resp.code) - assert_equal('hello from ipv6', resp.body) - end + test 'IPv6 address formatting logic - bare ::1' do + # Test the bracketing logic without needing real IPv6 networking + # Normalize address by stripping brackets if present + addr = '::1' + normalized_addr = if addr.start_with?('[') && addr.end_with?(']') + addr[1..-2] + else + addr + end + assert_equal('::1', normalized_addr) + + # Add brackets for URI formatting + addr_display = if normalized_addr.include?(":") + "[#{normalized_addr}]" + else + normalized_addr + end + assert_equal('[::1]', addr_display) + + # Verify URI construction works + uri = URI("http://#{addr_display}:24231/metrics").to_s + assert_equal('http://[::1]:24231/metrics', uri) end - test 'bind to IPv6 wildcard address' do - omit('IPv6 not supported') unless ipv6_enabled? - on_driver do |driver| - driver.http_server_create_http_server(:http_server_helper_test, addr: '::', port: @port, logger: NULL_LOGGER) do |s| - s.get('/example/hello') { [200, { 'Content-Type' => 'text/plain' }, 'hello from ipv6 wildcard'] } - end - # Can access via IPv4-mapped IPv6 or IPv6 loopback - resp = get("http://[::1]:#{@port}/example/hello") - assert_equal('200', resp.code) - assert_equal('hello from ipv6 wildcard', resp.body) - end + test 'IPv6 address formatting logic - bare ::' do + addr = '::' + normalized_addr = if addr.start_with?('[') && addr.end_with?(']') + addr[1..-2] + else + addr + end + assert_equal('::', normalized_addr) + + addr_display = if normalized_addr.include?(":") + "[#{normalized_addr}]" + else + normalized_addr + end + assert_equal('[::]', addr_display) + + uri = URI("http://#{addr_display}:24231/metrics").to_s + assert_equal('http://[::]:24231/metrics', uri) end - test 'handle already bracketed IPv6 address' do - omit('IPv6 not supported') unless ipv6_enabled? - on_driver do |driver| - # Test that pre-bracketed addresses don't get double-bracketed - driver.http_server_create_http_server(:http_server_helper_test, addr: '[::1]', port: @port, logger: NULL_LOGGER) do |s| - s.get('/example/hello') { [200, { 'Content-Type' => 'text/plain' }, 'hello from bracketed ipv6'] } - end - resp = get("http://[::1]:#{@port}/example/hello") - assert_equal('200', resp.code) - assert_equal('hello from bracketed ipv6', resp.body) - end + test 'IPv6 address formatting logic - already bracketed [::1]' do + # Test that pre-bracketed addresses are normalized correctly + addr = '[::1]' + # First normalize by removing brackets + normalized_addr = if addr.start_with?('[') && addr.end_with?(']') + addr[1..-2] + else + addr + end + assert_equal('::1', normalized_addr) # Brackets stripped + + # Then add brackets for URI + addr_display = if normalized_addr.include?(":") + "[#{normalized_addr}]" + else + normalized_addr + end + assert_equal('[::1]', addr_display) + + uri = URI("http://#{addr_display}:24231/metrics").to_s + assert_equal('http://[::1]:24231/metrics', uri) end - test 'handle already bracketed IPv6 wildcard address' do - omit('IPv6 not supported') unless ipv6_enabled? - on_driver do |driver| - driver.http_server_create_http_server(:http_server_helper_test, addr: '[::]', port: @port, logger: NULL_LOGGER) do |s| - s.get('/example/hello') { [200, { 'Content-Type' => 'text/plain' }, 'hello from bracketed ipv6 wildcard'] } - end - # Access via IPv6 loopback - resp = get("http://[::1]:#{@port}/example/hello") - assert_equal('200', resp.code) - assert_equal('hello from bracketed ipv6 wildcard', resp.body) - end + test 'IPv6 address formatting logic - already bracketed [::]' do + addr = '[::]' + # First normalize by removing brackets + normalized_addr = if addr.start_with?('[') && addr.end_with?(']') + addr[1..-2] + else + addr + end + assert_equal('::', normalized_addr) # Brackets stripped + + # Then add brackets for URI + addr_display = if normalized_addr.include?(":") + "[#{normalized_addr}]" + else + normalized_addr + end + assert_equal('[::]', addr_display) + + uri = URI("http://#{addr_display}:24231/metrics").to_s + assert_equal('http://[::]:24231/metrics', uri) + end + + test 'IPv4 address formatting logic - no brackets' do + addr = '127.0.0.1' + # Normalize (no-op for IPv4) + normalized_addr = if addr.start_with?('[') && addr.end_with?(']') + addr[1..-2] + else + addr + end + assert_equal('127.0.0.1', normalized_addr) + + # No brackets added for IPv4 + addr_display = if normalized_addr.include?(":") + "[#{normalized_addr}]" + else + normalized_addr + end + assert_equal('127.0.0.1', addr_display) # No brackets for IPv4 + + uri = URI("http://#{addr_display}:24231/metrics").to_s + assert_equal('http://127.0.0.1:24231/metrics', uri) end test 'must be called #start and #stop' do From d7fcf395719f7dd9da3dc8b8d93310ca23e7adb8 Mon Sep 17 00:00:00 2001 From: Jesse Awan Date: Thu, 5 Feb 2026 16:55:00 +0100 Subject: [PATCH 4/8] refactor: Address code review feedback - Rename addr_display to ip_literal per RFC 3986 terminology - Move ipv6_enabled? helper to test/helper.rb for reusability - Improve IPv6 detection with proper socket binding and address resolution checks Signed-off-by: Jesse Awan --- lib/fluent/plugin_helper/http_server/server.rb | 14 +++++++------- test/helper.rb | 11 +++++++++-- test/plugin_helper/test_http_server_helper.rb | 16 ---------------- 3 files changed, 16 insertions(+), 25 deletions(-) diff --git a/lib/fluent/plugin_helper/http_server/server.rb b/lib/fluent/plugin_helper/http_server/server.rb index d68939b40a..1fdcab39a0 100644 --- a/lib/fluent/plugin_helper/http_server/server.rb +++ b/lib/fluent/plugin_helper/http_server/server.rb @@ -44,13 +44,13 @@ def initialize(addr:, port:, logger:, default_app: nil, tls_context: nil) # TODO: support http2 scheme = tls_context ? 'https' : 'http' # Handle IPv6 addresses properly in URI construction per RFC 3986 - # Add brackets to IPv6 addresses for URI compliance - addr_display = if @addr.include?(":") - "[#{@addr}]" # IPv6 address - add brackets - else - @addr # IPv4 or hostname - use directly - end - @uri = URI("#{scheme}://#{addr_display}:#{@port}").to_s + # Add brackets to IPv6 addresses for URI compliance (ip-literal per RFC 3986) + ip_literal = if @addr.include?(":") + "[#{@addr}]" # IPv6 address - add brackets + else + @addr # IPv4 or hostname - use directly + end + @uri = URI("#{scheme}://#{ip_literal}:#{@port}").to_s @router = Router.new(default_app) @server_task = nil Console.logger = Fluent::Log::ConsoleAdapter.wrap(@logger) diff --git a/test/helper.rb b/test/helper.rb index 4e9f028a9c..97b0f64917 100644 --- a/test/helper.rb +++ b/test/helper.rb @@ -161,9 +161,16 @@ def ipv6_enabled? require 'socket' begin - TCPServer.open("::1", 0) + # Try to actually bind to an IPv6 address to verify it works + sock = Socket.new(Socket::AF_INET6, Socket::SOCK_STREAM, 0) + sock.bind(Socket.sockaddr_in(0, '::1')) + sock.close + + # Also test that we can resolve IPv6 addresses + # This is needed because some systems can bind but can't connect + Socket.getaddrinfo('::1', nil, Socket::AF_INET6) true - rescue + rescue Errno::EADDRNOTAVAIL, Errno::EAFNOSUPPORT, SocketError false end end diff --git a/test/plugin_helper/test_http_server_helper.rb b/test/plugin_helper/test_http_server_helper.rb index 35d57f17c0..99fd373a55 100644 --- a/test/plugin_helper/test_http_server_helper.rb +++ b/test/plugin_helper/test_http_server_helper.rb @@ -21,22 +21,6 @@ def teardown @port = nil end - def ipv6_enabled? - begin - # Try to actually bind to an IPv6 address to verify it works - sock = Socket.new(Socket::AF_INET6, Socket::SOCK_STREAM, 0) - sock.bind(Socket.sockaddr_in(0, '::1')) - sock.close - - # Also test that we can resolve IPv6 addresses - # This is needed because some systems can bind but can't connect - Socket.getaddrinfo('::1', nil, Socket::AF_INET6) - true - rescue Errno::EADDRNOTAVAIL, Errno::EAFNOSUPPORT, SocketError - false - end - end - class Dummy < Fluent::Plugin::TestBase helpers :http_server end From ed29cdeae30fbd9474a651631efa674822e81bb2 Mon Sep 17 00:00:00 2001 From: Jesse Awan Date: Fri, 6 Feb 2026 10:20:17 +0100 Subject: [PATCH 5/8] tests: Add IPv6 integration tests and improve ipv6_enabled? detection Signed-off-by: Jesse Awan --- test/plugin_helper/test_http_server_helper.rb | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/test/plugin_helper/test_http_server_helper.rb b/test/plugin_helper/test_http_server_helper.rb index 99fd373a55..042542d723 100644 --- a/test/plugin_helper/test_http_server_helper.rb +++ b/test/plugin_helper/test_http_server_helper.rb @@ -21,6 +21,22 @@ def teardown @port = nil end + def ipv6_enabled? + begin + # Try to actually bind to an IPv6 address to verify it works + sock = Socket.new(Socket::AF_INET6, Socket::SOCK_STREAM, 0) + sock.bind(Socket.sockaddr_in(0, '::1')) + sock.close + + # Also test that we can resolve IPv6 addresses + # This is needed because some systems can bind but can't connect + Socket.getaddrinfo('::1', nil, Socket::AF_INET6) + true + rescue Errno::EADDRNOTAVAIL, Errno::EAFNOSUPPORT, SocketError + false + end + end + class Dummy < Fluent::Plugin::TestBase helpers :http_server end @@ -474,6 +490,53 @@ def start_https_request(addr, port, verify: true, cert_path: nil, selfsigned: tr uri = URI("http://#{addr_display}:24231/metrics").to_s assert_equal('http://127.0.0.1:24231/metrics', uri) end + + test 'http_server can bind and serve on IPv6 loopback ::1' do + return unless ipv6_enabled? + + on_driver do |driver| + driver.http_server_create_http_server(:ipv6_loopback_test, addr: '::1', port: @port, logger: NULL_LOGGER) do |s| + s.get('/test') { [200, { 'Content-Type' => 'text/plain' }, 'OK'] } + end + + # Use Net::HTTP directly with proper IPv6 handling + http = Net::HTTP.new('::1', @port) + response = http.get('/test') + assert_equal('200', response.code) + assert_equal('OK', response.body) + end + end + + test 'http_server can bind on IPv6 any address ::' do + return unless ipv6_enabled? + + on_driver do |driver| + driver.http_server_create_http_server(:ipv6_any_test, addr: '::', port: @port, logger: NULL_LOGGER) do |s| + s.get('/test') { [200, { 'Content-Type' => 'text/plain' }, 'OK'] } + end + + # Connect via loopback to test the :: bind worked + http = Net::HTTP.new('::1', @port) + response = http.get('/test') + assert_equal('200', response.code) + assert_equal('OK', response.body) + end + end + + test 'http_server handles pre-bracketed IPv6 address [::1]' do + return unless ipv6_enabled? + + on_driver do |driver| + driver.http_server_create_http_server(:ipv6_bracketed_test, addr: '[::1]', port: @port, logger: NULL_LOGGER) do |s| + s.get('/test') { [200, { 'Content-Type' => 'text/plain' }, 'OK'] } + end + + http = Net::HTTP.new('::1', @port) + response = http.get('/test') + assert_equal('200', response.code) + assert_equal('OK', response.body) + end + end test 'must be called #start and #stop' do on_driver do |driver| From 56aa26973025d3ce0b86aa7c9e7e180523fe7cff Mon Sep 17 00:00:00 2001 From: Jesse Awan Date: Fri, 6 Feb 2026 10:22:31 +0100 Subject: [PATCH 6/8] Remove duplicate ipv6_enabled? - use global helper from test/helper.rb Signed-off-by: Jesse Awan --- test/plugin_helper/test_http_server_helper.rb | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/test/plugin_helper/test_http_server_helper.rb b/test/plugin_helper/test_http_server_helper.rb index 042542d723..5ba54b2e77 100644 --- a/test/plugin_helper/test_http_server_helper.rb +++ b/test/plugin_helper/test_http_server_helper.rb @@ -21,22 +21,6 @@ def teardown @port = nil end - def ipv6_enabled? - begin - # Try to actually bind to an IPv6 address to verify it works - sock = Socket.new(Socket::AF_INET6, Socket::SOCK_STREAM, 0) - sock.bind(Socket.sockaddr_in(0, '::1')) - sock.close - - # Also test that we can resolve IPv6 addresses - # This is needed because some systems can bind but can't connect - Socket.getaddrinfo('::1', nil, Socket::AF_INET6) - true - rescue Errno::EADDRNOTAVAIL, Errno::EAFNOSUPPORT, SocketError - false - end - end - class Dummy < Fluent::Plugin::TestBase helpers :http_server end From 655e86fc2d95616abf11162246279eae0012dc96 Mon Sep 17 00:00:00 2001 From: Jesse Awan Date: Fri, 6 Feb 2026 10:39:01 +0100 Subject: [PATCH 7/8] Remove redundant IPv6 formatting logic tests - covered by integration tests Signed-off-by: Jesse Awan --- test/plugin_helper/test_http_server_helper.rb | 111 ------------------ 1 file changed, 111 deletions(-) diff --git a/test/plugin_helper/test_http_server_helper.rb b/test/plugin_helper/test_http_server_helper.rb index 5ba54b2e77..b4bf641be6 100644 --- a/test/plugin_helper/test_http_server_helper.rb +++ b/test/plugin_helper/test_http_server_helper.rb @@ -363,117 +363,6 @@ def start_https_request(addr, port, verify: true, cert_path: nil, selfsigned: tr end end end - - test 'IPv6 address formatting logic - bare ::1' do - # Test the bracketing logic without needing real IPv6 networking - # Normalize address by stripping brackets if present - addr = '::1' - normalized_addr = if addr.start_with?('[') && addr.end_with?(']') - addr[1..-2] - else - addr - end - assert_equal('::1', normalized_addr) - - # Add brackets for URI formatting - addr_display = if normalized_addr.include?(":") - "[#{normalized_addr}]" - else - normalized_addr - end - assert_equal('[::1]', addr_display) - - # Verify URI construction works - uri = URI("http://#{addr_display}:24231/metrics").to_s - assert_equal('http://[::1]:24231/metrics', uri) - end - - test 'IPv6 address formatting logic - bare ::' do - addr = '::' - normalized_addr = if addr.start_with?('[') && addr.end_with?(']') - addr[1..-2] - else - addr - end - assert_equal('::', normalized_addr) - - addr_display = if normalized_addr.include?(":") - "[#{normalized_addr}]" - else - normalized_addr - end - assert_equal('[::]', addr_display) - - uri = URI("http://#{addr_display}:24231/metrics").to_s - assert_equal('http://[::]:24231/metrics', uri) - end - - test 'IPv6 address formatting logic - already bracketed [::1]' do - # Test that pre-bracketed addresses are normalized correctly - addr = '[::1]' - # First normalize by removing brackets - normalized_addr = if addr.start_with?('[') && addr.end_with?(']') - addr[1..-2] - else - addr - end - assert_equal('::1', normalized_addr) # Brackets stripped - - # Then add brackets for URI - addr_display = if normalized_addr.include?(":") - "[#{normalized_addr}]" - else - normalized_addr - end - assert_equal('[::1]', addr_display) - - uri = URI("http://#{addr_display}:24231/metrics").to_s - assert_equal('http://[::1]:24231/metrics', uri) - end - - test 'IPv6 address formatting logic - already bracketed [::]' do - addr = '[::]' - # First normalize by removing brackets - normalized_addr = if addr.start_with?('[') && addr.end_with?(']') - addr[1..-2] - else - addr - end - assert_equal('::', normalized_addr) # Brackets stripped - - # Then add brackets for URI - addr_display = if normalized_addr.include?(":") - "[#{normalized_addr}]" - else - normalized_addr - end - assert_equal('[::]', addr_display) - - uri = URI("http://#{addr_display}:24231/metrics").to_s - assert_equal('http://[::]:24231/metrics', uri) - end - - test 'IPv4 address formatting logic - no brackets' do - addr = '127.0.0.1' - # Normalize (no-op for IPv4) - normalized_addr = if addr.start_with?('[') && addr.end_with?(']') - addr[1..-2] - else - addr - end - assert_equal('127.0.0.1', normalized_addr) - - # No brackets added for IPv4 - addr_display = if normalized_addr.include?(":") - "[#{normalized_addr}]" - else - normalized_addr - end - assert_equal('127.0.0.1', addr_display) # No brackets for IPv4 - - uri = URI("http://#{addr_display}:24231/metrics").to_s - assert_equal('http://127.0.0.1:24231/metrics', uri) - end test 'http_server can bind and serve on IPv6 loopback ::1' do return unless ipv6_enabled? From 2e6d0cb2cce5fca851a414532e35c942b96024c1 Mon Sep 17 00:00:00 2001 From: Jesse Awan Date: Mon, 9 Feb 2026 19:01:50 +0100 Subject: [PATCH 8/8] test: Refactor IPv6 tests using data-driven pattern Reduce code duplication by combining three separate IPv6 test methods into a single parametrized test using the data() pattern as suggested by reviewer. Signed-off-by: Jesse Awan --- test/plugin_helper/test_http_server_helper.rb | 43 ++++--------------- 1 file changed, 9 insertions(+), 34 deletions(-) diff --git a/test/plugin_helper/test_http_server_helper.rb b/test/plugin_helper/test_http_server_helper.rb index b4bf641be6..d206367c95 100644 --- a/test/plugin_helper/test_http_server_helper.rb +++ b/test/plugin_helper/test_http_server_helper.rb @@ -364,47 +364,22 @@ def start_https_request(addr, port, verify: true, cert_path: nil, selfsigned: tr end end - test 'http_server can bind and serve on IPv6 loopback ::1' do + data( + 'IPv6 loopback ::1' => [:ipv6_loopback_test, '::1', '::1'], + 'IPv6 any address ::' => [:ipv6_any_test, '::', '::1'], + 'pre-bracketed IPv6 [::1]' => [:ipv6_bracketed_test, '[::1]', '::1'] + ) + test 'http_server can bind and serve on IPv6 address' do |data| return unless ipv6_enabled? - on_driver do |driver| - driver.http_server_create_http_server(:ipv6_loopback_test, addr: '::1', port: @port, logger: NULL_LOGGER) do |s| - s.get('/test') { [200, { 'Content-Type' => 'text/plain' }, 'OK'] } - end - - # Use Net::HTTP directly with proper IPv6 handling - http = Net::HTTP.new('::1', @port) - response = http.get('/test') - assert_equal('200', response.code) - assert_equal('OK', response.body) - end - end - - test 'http_server can bind on IPv6 any address ::' do - return unless ipv6_enabled? - - on_driver do |driver| - driver.http_server_create_http_server(:ipv6_any_test, addr: '::', port: @port, logger: NULL_LOGGER) do |s| - s.get('/test') { [200, { 'Content-Type' => 'text/plain' }, 'OK'] } - end - - # Connect via loopback to test the :: bind worked - http = Net::HTTP.new('::1', @port) - response = http.get('/test') - assert_equal('200', response.code) - assert_equal('OK', response.body) - end - end - - test 'http_server handles pre-bracketed IPv6 address [::1]' do - return unless ipv6_enabled? + test_name, bind_addr, connect_addr = data on_driver do |driver| - driver.http_server_create_http_server(:ipv6_bracketed_test, addr: '[::1]', port: @port, logger: NULL_LOGGER) do |s| + driver.http_server_create_http_server(test_name, addr: bind_addr, port: @port, logger: NULL_LOGGER) do |s| s.get('/test') { [200, { 'Content-Type' => 'text/plain' }, 'OK'] } end - http = Net::HTTP.new('::1', @port) + http = Net::HTTP.new(connect_addr, @port) response = http.get('/test') assert_equal('200', response.code) assert_equal('OK', response.body)