Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# v1.19

## Release v1.19.1 - TBD

### Bug fix

* http_server helper: Fix IPv6 bind address support in URI construction
* 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

## Release v1.19.0 - 2025/07/30

### Enhancement
Expand Down
11 changes: 10 additions & 1 deletion lib/fluent/plugin_helper/http_server/server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,16 @@ 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
# 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
Console.logger = Fluent::Log::ConsoleAdapter.wrap(@logger)
Expand Down
67 changes: 67 additions & 0 deletions test/plugin_helper/test_http_server_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -364,6 +380,57 @@ 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 '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|
Expand Down