From f9dfec1a0b0f351f603a6ab0348e8ab01e397871 Mon Sep 17 00:00:00 2001 From: "Yamashita, Yuu" Date: Sat, 31 Oct 2026 23:11:55 +0900 Subject: [PATCH 01/12] ci: enforce frozen string literals across Ruby matrix --- .github/workflows/test.yml | 36 +++++++++++++----------------------- 1 file changed, 13 insertions(+), 23 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 723bcd0d..fa378bcd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -23,10 +23,11 @@ jobs: '3.3', '3.4', '4.0', - 'head' + 'head', + 'jruby-10.1' ] # Rake replaces RUBYOPT for the test subprocess; keep it in Bundler's context. - ruby-opt: ["-rbundler/setup"] + ruby-opt: ["-rbundler/setup --enable-frozen-string-literal --debug-frozen-string-literal"] include: # Ruby 2.5 through 3.1 have no prebuilt binaries for Ubuntu 26.04. - ruby-version: '2.5' @@ -49,39 +50,28 @@ jobs: os: ubuntu-latest - ruby-version: 'head' os: ubuntu-latest - - ruby-version: '3.4' - ruby-opt: '-rbundler/setup --enable-frozen-string-literal --debug-frozen-string-literal' + - ruby-version: 'jruby-10.1' os: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Set up Ruby - uses: ruby/setup-ruby@v1 - with: - ruby-version: ${{ matrix.ruby-version }} - bundler-cache: true - cache-version: 2 - - name: Run tests - run: bundle exec rake --trace RUBYOPT="${{ matrix.ruby-opt }}" - - jruby: - runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Java + if: startsWith(matrix.ruby-version, 'jruby-') uses: actions/setup-java@v6 with: distribution: temurin java-version: '21' - - name: Set up JRuby + - name: Set up Ruby uses: ruby/setup-ruby@v1 with: - ruby-version: jruby-10.1.2.0 + ruby-version: ${{ matrix.ruby-version }} bundler-cache: true cache-version: 2 - name: Run tests - run: bundle exec rake --trace RUBYOPT="-rbundler/setup" - - name: Run JRuby-specific tests - run: bundle exec ruby -Ilib test/jruby_ssl_socket/test_pemutils.rb + run: bundle exec rake --trace RUBYOPT="${{ matrix.ruby-opt }}" + - name: Run JRuby-specific PEM tests + if: startsWith(matrix.ruby-version, 'jruby-') + run: RUBYOPT="${{ matrix.ruby-opt }}" bundle exec ruby -Ilib test/jruby_ssl_socket/test_pemutils.rb - name: Run JRuby socket timeout tests - run: bundle exec ruby -Ilib test/jruby_ssl_socket/test_socket_timeout.rb + if: startsWith(matrix.ruby-version, 'jruby-') + run: RUBYOPT="${{ matrix.ruby-opt }}" bundle exec ruby -Ilib test/jruby_ssl_socket/test_socket_timeout.rb From 57e95f168b4c8e524dc9c2b87968a3b527beb444 Mon Sep 17 00:00:00 2001 From: "Yamashita, Yuu" Date: Sat, 31 Oct 2026 23:43:30 +0900 Subject: [PATCH 02/12] test: run JRuby-specific suites through rake --- .github/workflows/test.yml | 6 ------ Rakefile | 2 +- test/jruby_ssl_socket/test_pemutils.rb | 1 + test/jruby_ssl_socket/test_socket_timeout.rb | 1 + 4 files changed, 3 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fa378bcd..538e7c8b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -69,9 +69,3 @@ jobs: cache-version: 2 - name: Run tests run: bundle exec rake --trace RUBYOPT="${{ matrix.ruby-opt }}" - - name: Run JRuby-specific PEM tests - if: startsWith(matrix.ruby-version, 'jruby-') - run: RUBYOPT="${{ matrix.ruby-opt }}" bundle exec ruby -Ilib test/jruby_ssl_socket/test_pemutils.rb - - name: Run JRuby socket timeout tests - if: startsWith(matrix.ruby-version, 'jruby-') - run: RUBYOPT="${{ matrix.ruby-opt }}" bundle exec ruby -Ilib test/jruby_ssl_socket/test_socket_timeout.rb diff --git a/Rakefile b/Rakefile index 73d219ba..324b2952 100644 --- a/Rakefile +++ b/Rakefile @@ -9,7 +9,7 @@ ENV['CI_REPORTS'] = File.expand_path('./reports', File.dirname(__FILE__)) Rake::TestTask.new('test') do |test| test.libs << 'lib' test.verbose = true - test.test_files = Dir.glob('test/test_*.rb') + test.test_files = Dir.glob('test/**/test_*.rb') end unless ENV['CI'] diff --git a/test/jruby_ssl_socket/test_pemutils.rb b/test/jruby_ssl_socket/test_pemutils.rb index 560a1c6e..00d2ce49 100644 --- a/test/jruby_ssl_socket/test_pemutils.rb +++ b/test/jruby_ssl_socket/test_pemutils.rb @@ -5,6 +5,7 @@ class PEMUtilsTest < Test::Unit::TestCase include Helper def setup + omit('JRuby-specific test') unless RUBY_ENGINE == 'jruby' @raw_cert = "-----BEGIN CERTIFICATE-----\nMIIDOTCCAiGgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBCMRMwEQYKCZImiZPyLGQB\nGRYDb3JnMRkwFwYKCZImiZPyLGQBGRYJcnVieS1sYW5nMRAwDgYDVQQDDAdSdWJ5\nIENBMB4XDTE2MDgxMDE3MjEzNFoXDTE3MDgxMDE3MjEzNFowSzETMBEGCgmSJomT\n8ixkARkWA29yZzEZMBcGCgmSJomT8ixkARkWCXJ1YnktbGFuZzEZMBcGA1UEAwwQ\nUnVieSBjZXJ0aWZpY2F0ZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB\nAJCfsSXpSMpmZCVa+ZCM+QDgomnhDlvnrGDq6pasTaIspGTXgws+7r8Dt/cNe6EH\nHJpRH2cGRiO4yPcfcT9eS4X7k8OC4f33wHfACOmLu6LeoNE8ujmSk6L6WzLUI+sE\nnLZbFrXxoAo4XHsm8vEG9C+jEoXZ1p+47wrAGaDwDQTnzlMy4dT9pRQEJP2G/Rry\nUkuZn8SUWmh3/YS78iaSzsNF1cgE1ealHOrPPFDjiCGDaH/LHyUPYlbFSLZ/B7Qx\nLxi5sePLcywWq/EJrmWpgeVTDjtNijsdKv/A3qkY+fm/oD0pzt7XsfJaP9YKNyJO\nQFdxWZeiPcDF+Hwf+IwSr+kCAwEAAaMxMC8wDgYDVR0PAQH/BAQDAgeAMB0GA1Ud\nDgQWBBQNvzYzJyXemGhxbA8NMXLolDnPyjANBgkqhkiG9w0BAQsFAAOCAQEARIJV\noKejGlOTn71QutnNnu07UtTu0IHs6YqjYzzND+m4JXLN+wvYm72AFUG0b1L7dRg0\niK8XjQrlNQNVqP1Mc6tffchy20neOPOHeiO6qTdRU8P2S8D3Uwe+1qhgxjfE+cWc\nwZmWxYK4HA8c58PxWMqrkr2QqXDplG9KWLvOgrtPGiLLZcQSKhvvB63QzItHBDU6\nRayiJY3oPkK/HrIvFlySqFqzWmuyknkciOFywEHQMz/tcSFJ2QFpPj/tBz9VXohH\nZ8KscmfhZrTPBjo+ky1lz/WraWoz4LMiLnkC2ABczWLRSawu+v3Irx1NFJngt05e\npqwtqIUeg7j+JLiTaA==\n-----END CERTIFICATE-----" end diff --git a/test/jruby_ssl_socket/test_socket_timeout.rb b/test/jruby_ssl_socket/test_socket_timeout.rb index 8b854431..9202a6f9 100644 --- a/test/jruby_ssl_socket/test_socket_timeout.rb +++ b/test/jruby_ssl_socket/test_socket_timeout.rb @@ -3,6 +3,7 @@ class JRubySocketTimeoutTest < Test::Unit::TestCase def test_read_timeout_is_reported_as_receive_timeout + omit('JRuby-specific test') unless RUBY_ENGINE == 'jruby' socket = HTTPClient::JavaSocketWrap.allocate input = Object.new input.define_singleton_method(:read) do |_buffer| From 57eb19b2ad8a3aa83a9453ecac464ac87b7baf87 Mon Sep 17 00:00:00 2001 From: "Yamashita, Yuu" Date: Sat, 31 Oct 2026 23:43:31 +0900 Subject: [PATCH 03/12] ci: test JRuby head and use Temurin 25 --- .github/workflows/test.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 538e7c8b..89e28622 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,7 +24,8 @@ jobs: '3.4', '4.0', 'head', - 'jruby-10.1' + 'jruby-10.1', + 'jruby-head' ] # Rake replaces RUBYOPT for the test subprocess; keep it in Bundler's context. ruby-opt: ["-rbundler/setup --enable-frozen-string-literal --debug-frozen-string-literal"] @@ -52,6 +53,8 @@ jobs: os: ubuntu-latest - ruby-version: 'jruby-10.1' os: ubuntu-latest + - ruby-version: 'jruby-head' + os: ubuntu-latest steps: - uses: actions/checkout@v3 @@ -60,7 +63,7 @@ jobs: uses: actions/setup-java@v6 with: distribution: temurin - java-version: '21' + java-version: '25' - name: Set up Ruby uses: ruby/setup-ruby@v1 with: From f69e74d4f15964747e1d9acaf1a50757f0c46985 Mon Sep 17 00:00:00 2001 From: "Yamashita, Yuu" Date: Sat, 31 Oct 2026 23:52:09 +0900 Subject: [PATCH 04/12] fix: keep JRuby buffers mutable with frozen string literals Duplicate socket and PEM buffers before mutating them, and copy binary debug input before changing its encoding. Add regression coverage for buffer reads, certificate chains, and frozen or mutable debug input. --- lib/httpclient/jruby_ssl_socket.rb | 4 +-- lib/httpclient/session.rb | 1 + test/jruby_ssl_socket/test_mutable_buffers.rb | 30 +++++++++++++++++++ test/test_httpclient.rb | 15 ++++++++++ 4 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 test/jruby_ssl_socket/test_mutable_buffers.rb diff --git a/lib/httpclient/jruby_ssl_socket.rb b/lib/httpclient/jruby_ssl_socket.rb index ae2a8b0d..d4b56731 100644 --- a/lib/httpclient/jruby_ssl_socket.rb +++ b/lib/httpclient/jruby_ssl_socket.rb @@ -42,7 +42,7 @@ def initialize(socket, debug_dev = nil) @outstr = @socket.getOutputStream @instr = BufferedInputStream.new(@socket.getInputStream) @buf = (' ' * BUF_SIZE).to_java_bytes - @bufstr = '' + @bufstr = ''.dup end def close @@ -352,7 +352,7 @@ def add(cert_source) File.read(cert_source).each_line do |line| case line when /-----BEGIN CERTIFICATE-----/ - pem = '' + pem = ''.dup when /-----END CERTIFICATE-----/ load_pem(pem) # keep parsing in case where multiple certificates in a file diff --git a/lib/httpclient/session.rb b/lib/httpclient/session.rb index 8cb09d4d..29c6f93c 100644 --- a/lib/httpclient/session.rb +++ b/lib/httpclient/session.rb @@ -400,6 +400,7 @@ def debug(str) if str && @debug_dev if str.index("\0") require 'hexdump' + str = str.dup str.force_encoding('BINARY') if str.respond_to?(:force_encoding) @debug_dev << HexDump.encode(str).join("\n") else diff --git a/test/jruby_ssl_socket/test_mutable_buffers.rb b/test/jruby_ssl_socket/test_mutable_buffers.rb new file mode 100644 index 00000000..b8dcd029 --- /dev/null +++ b/test/jruby_ssl_socket/test_mutable_buffers.rb @@ -0,0 +1,30 @@ +require File.expand_path('../helper', File.dirname(__FILE__)) + + +class JRubyMutableBuffersTest < Test::Unit::TestCase + def setup + omit('JRuby-specific test') unless RUBY_ENGINE == 'jruby' + end + + def test_socket_reads_using_mutable_buffer + input = Java::JavaIo::ByteArrayInputStream.new("hello\nworld".to_java_bytes) + output = Java::JavaIo::ByteArrayOutputStream.new + socket = Object.new + socket.define_singleton_method(:getInputStream) { input } + socket.define_singleton_method(:getOutputStream) { output } + wrapped_socket = HTTPClient::JavaSocketWrap.new(socket) + + assert_equal("hello\n", wrapped_socket.gets("\n")) + assert_equal('wo', wrapped_socket.read(2)) + assert_equal('rld', wrapped_socket.readpartial(3)) + assert_raise(EOFError) { wrapped_socket.readpartial(1) } + end + + def test_trust_store_loads_multiple_pem_certificates + loader = HTTPClient::JRubySSLSocket::TrustStoreLoader.new + loader.add(File.expand_path('../ca-chain.pem', File.dirname(__FILE__))) + + assert_equal(2, loader.size) + assert_equal(2, loader.trust_store.size) + end +end diff --git a/test/test_httpclient.rb b/test/test_httpclient.rb index 74406ef2..e13485eb 100644 --- a/test/test_httpclient.rb +++ b/test/test_httpclient.rb @@ -65,6 +65,21 @@ def test_debug_dev_stream assert(!str.empty?) end + data('mutable' => false, 'frozen' => true) + def test_debug_socket_does_not_mutate_binary_input(freeze_payload) + debug_dev = StringIO.new + socket = Object.new + socket.extend(HTTPClient::DebugSocket) + socket.debug_dev = debug_dev + payload = "\x00".dup + payload.freeze if freeze_payload + + assert_nothing_raised { socket.send(:debug, payload) } + assert_equal("\x00", payload) + assert_equal(Encoding::UTF_8, payload.encoding) + assert(!debug_dev.string.empty?) + end + def test_protocol_version_http09 @client.protocol_version = 'HTTP/0.9' @client.debug_dev = str = ''.dup From 8b5642c000c9943a21215a4ad6bd7249b960e99a Mon Sep 17 00:00:00 2001 From: "Yamashita, Yuu" Date: Sun, 1 Nov 2026 00:48:02 +0900 Subject: [PATCH 05/12] ci: update checkout action to v7 --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 89e28622..445570be 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -57,7 +57,7 @@ jobs: os: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v7 - name: Set up Java if: startsWith(matrix.ruby-version, 'jruby-') uses: actions/setup-java@v6 From bc1e16aa45f08a8e12c9b0b3f23f5bd65bb6ade5 Mon Sep 17 00:00:00 2001 From: "Yamashita, Yuu" Date: Sun, 1 Nov 2026 01:10:35 +0900 Subject: [PATCH 06/12] test: remove unused PEM result assignments --- test/jruby_ssl_socket/test_pemutils.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/jruby_ssl_socket/test_pemutils.rb b/test/jruby_ssl_socket/test_pemutils.rb index 00d2ce49..66915446 100644 --- a/test/jruby_ssl_socket/test_pemutils.rb +++ b/test/jruby_ssl_socket/test_pemutils.rb @@ -11,21 +11,21 @@ def setup def test_read_certificate assert_nothing_raised do - binary = HTTPClient::JRubySSLSocket::PEMUtils.read_certificate(@raw_cert) + HTTPClient::JRubySSLSocket::PEMUtils.read_certificate(@raw_cert) end end def test_read_certificate_works_with_random_ascii_text_outside_begin_end raw_cert_with_ascii = "some text before begin\n" + @raw_cert + "\nsome text after end" assert_nothing_raised do - binary = HTTPClient::JRubySSLSocket::PEMUtils.read_certificate(raw_cert_with_ascii) + HTTPClient::JRubySSLSocket::PEMUtils.read_certificate(raw_cert_with_ascii) end end def test_read_certificate_uses_all_content_if_missing_begin_end cert = @raw_cert.sub(/-----BEGIN CERTIFICATE-----/, '').sub(/-----END CERTIFICATE-----/, '') assert_nothing_raised do - binary = HTTPClient::JRubySSLSocket::PEMUtils.read_certificate(cert) + HTTPClient::JRubySSLSocket::PEMUtils.read_certificate(cert) end end From dfa2ce932a7b854de71a7d733dc20d774b1c88e0 Mon Sep 17 00:00:00 2001 From: "Yamashita, Yuu" Date: Sun, 1 Nov 2026 01:10:52 +0900 Subject: [PATCH 07/12] test: match multipart headers as binary data --- test/test_httpclient.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_httpclient.rb b/test/test_httpclient.rb index e13485eb..aa78b019 100644 --- a/test/test_httpclient.rb +++ b/test/test_httpclient.rb @@ -949,7 +949,7 @@ def test_post_with_file STDOUT.sync = true File.open(__FILE__) do |file| res = @client.post(serverurl + 'servlet', {1=>2, 3=>file}) - assert_match(/^Content-Disposition: form-data; name="1"\r\n/nm, res.content) + assert_match(/^Content-Disposition: form-data; name="1"\r\n/m, res.content.b) assert_match(/^Content-Disposition: form-data; name="3";/, res.content) assert_match(/FIND_TAG_IN_THIS_FILE/, res.content) end From 47ad028244dc19a13f06b8c4811a375ec8c180b7 Mon Sep 17 00:00:00 2001 From: "Yamashita, Yuu" Date: Sun, 1 Nov 2026 01:11:14 +0900 Subject: [PATCH 08/12] test: silence deprecated encoding setter warnings --- test/test_httpclient.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/test_httpclient.rb b/test/test_httpclient.rb index aa78b019..d1d664ca 100644 --- a/test/test_httpclient.rb +++ b/test/test_httpclient.rb @@ -783,7 +783,8 @@ def test_get_async_for_largebody if RUBY_VERSION > "1.9" def test_post_async_with_default_internal original_encoding = Encoding.default_internal - Encoding.default_internal = Encoding::UTF_8 + # This test covers the legacy setter even though JRuby deprecates it. + silent { Encoding.default_internal = Encoding::UTF_8 } begin post_body = StringIO.new("こんにちは") conn = @client.post_async(serverurl + 'servlet', post_body) @@ -791,7 +792,7 @@ def test_post_async_with_default_internal res = conn.pop assert_equal 'post,こんにちは', res.content.read ensure - Encoding.default_internal = original_encoding + silent { Encoding.default_internal = original_encoding } end end end From bc5942aea69b9fc8efd5c5bfe91ca1eede991d56 Mon Sep 17 00:00:00 2001 From: "Yamashita, Yuu" Date: Sun, 1 Nov 2026 01:11:33 +0900 Subject: [PATCH 09/12] test: require CGI escape support directly --- test/helper.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/helper.rb b/test/helper.rb index e58c2eb7..e83a62d7 100644 --- a/test/helper.rb +++ b/test/helper.rb @@ -6,7 +6,11 @@ require 'webrick/httpproxy.rb' require 'logger' require 'stringio' -require 'cgi' +begin + require 'cgi/escape' +rescue LoadError + require 'cgi' +end require 'webrick/httputils' if RUBY_ENGINE == 'jruby' From 0ebba7b52f19673a9277fd20ef410d5a745abedb Mon Sep 17 00:00:00 2001 From: "Yamashita, Yuu" Date: Sun, 1 Nov 2026 01:11:57 +0900 Subject: [PATCH 10/12] test: isolate legacy NTLM dependency warning --- test/test_auth.rb | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/test/test_auth.rb b/test/test_auth.rb index 95c17188..8b97e914 100644 --- a/test/test_auth.rb +++ b/test/test_auth.rb @@ -2,7 +2,14 @@ require 'digest/md5' require 'rack' require 'rack/lint' -require 'rack-ntlm' +# rack-ntlm-test-service 0.0.1 assigns an unused rescue variable while loading. +original_verbose = $VERBOSE +begin + $VERBOSE = nil + require 'rack-ntlm' +ensure + $VERBOSE = original_verbose +end class TestAuth < Test::Unit::TestCase include Helper From b792ede38272ef610e6d567c001de476fa9c440c Mon Sep 17 00:00:00 2001 From: "Yamashita, Yuu" Date: Sun, 1 Nov 2026 01:12:19 +0900 Subject: [PATCH 11/12] test: skip absent JRuby peer certificate chains --- test/helper.rb | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/test/helper.rb b/test/helper.rb index e83a62d7..969bb369 100644 --- a/test/helper.rb +++ b/test/helper.rb @@ -40,9 +40,24 @@ def run(sock) sock.singleton_class.remove_method(:to_io) if readable_io end end + + module HTTPRequestPatch + def parse(socket = nil) + skip_peer_cert_chain = socket.respond_to?(:peer_cert_chain) && + socket.respond_to?(:peer_cert) && socket.peer_cert.nil? + if skip_peer_cert_chain + # WEBrick asks JRuby for a peer chain even when no client certificate was sent. + socket.define_singleton_method(:peer_cert_chain) { nil } + end + super + ensure + socket.singleton_class.remove_method(:peer_cert_chain) if skip_peer_cert_chain + end + end end WEBrick::HTTPServer.prepend(JRubyWEBrickSSLReadBuffer::HTTPServerPatch) + WEBrick::HTTPRequest.prepend(JRubyWEBrickSSLReadBuffer::HTTPRequestPatch) end From b9d6b2ac601682a52eaf61446beeb3bcbba21ee8 Mon Sep 17 00:00:00 2001 From: "Yamashita, Yuu" Date: Sun, 1 Nov 2026 01:38:45 +0900 Subject: [PATCH 12/12] test: load WEBrick HTTPS before prepending request patch WEBrick HTTPS aliases HTTPRequest#parse when loaded. Loading it after our prepend aliases the patch itself, causing recursive request parsing and StackOverflowError on JRuby. Require HTTPS first so orig_parse retains the original parser. Verified with two full JRuby 10.1.2.0 / Temurin 25 test runs: 256 tests, no failures or errors, and no Ruby warnings. --- test/helper.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/test/helper.rb b/test/helper.rb index 969bb369..7a3c3ba3 100644 --- a/test/helper.rb +++ b/test/helper.rb @@ -14,6 +14,7 @@ require 'webrick/httputils' if RUBY_ENGINE == 'jruby' + require 'webrick/https' require 'delegate' module JRubyWEBrickSSLReadBuffer