-
<%= request.render "/jsp/include.jsp", {"message" => "I'm a message that came from Sinatra!"} %>
<%= request.render "/jsp/include.jsp", {"message" => "I'm a message that came from Sinatra!"} %>
From 2b50205c7059425c18984aba1cc51aca225661f6 Mon Sep 17 00:00:00 2001 From: Chad Wilson <29788154+chadlwilson@users.noreply.github.com> Date: Tue, 19 Aug 2025 15:50:36 +0800 Subject: [PATCH 01/10] feat: implement compatibility with Rack 3.x specifications --- .github/workflows/maven.yml | 7 +- Appraisals | 5 +- CHANGELOG.md | 6 +- README.md | 5 +- Rakefile | 2 +- gemfiles/rails72_rack31.gemfile | 19 +++++ gemfiles/rails80_rack31.gemfile | 19 +++++ gemfiles/rails80_rack32.gemfile | 19 +++++ gemfiles/rails81_rack31.gemfile | 19 +++++ gemfiles/rails81_rack32.gemfile | 19 +++++ .../java/org/jruby/rack/RackEnvironment.java | 6 ++ src/main/ruby/jruby/rack/chunked.rb | 30 +++---- .../ruby/jruby/rack/error_app/show_status.rb | 1 - .../ruby/rack/handler/servlet/default_env.rb | 22 +++-- .../ruby/rack/handler/servlet/servlet_env.rb | 2 + src/spec/ruby/jruby/rack/booter_spec.rb | 2 +- src/spec/ruby/jruby/rack/integration_spec.rb | 22 ++++- .../rack/handler/servlet_env_parsing_spec.rb | 1 + .../ruby/rack/handler/servlet_lint_spec.rb | 6 +- src/spec/ruby/rack/handler/servlet_spec.rb | 84 +++++++++++++++---- .../app/controllers/application_controller.rb | 4 + .../rails81/app/helpers/application_helper.rb | 2 + src/spec/stub/rails81/config/application.rb | 42 ++++++++++ src/spec/stub/rails81/config/boot.rb | 3 + .../stub/rails81/config/credentials.yml.enc | 1 + src/spec/stub/rails81/config/environment.rb | 5 ++ .../config/environments/development.rb | 42 ++++++++++ .../rails81/config/environments/production.rb | 73 ++++++++++++++++ .../stub/rails81/config/environments/test.rb | 42 ++++++++++ .../initializers/content_security_policy.rb | 29 +++++++ .../initializers/filter_parameter_logging.rb | 8 ++ .../config/initializers/inflections.rb | 16 ++++ src/spec/stub/rails81/config/locales/en.yml | 31 +++++++ src/spec/stub/rails81/config/master.key | 1 + src/spec/stub/rails81/config/routes.rb | 14 ++++ src/spec/stub/rails81/public/robots.txt | 1 + 36 files changed, 560 insertions(+), 50 deletions(-) create mode 100644 gemfiles/rails72_rack31.gemfile create mode 100644 gemfiles/rails80_rack31.gemfile create mode 100644 gemfiles/rails80_rack32.gemfile create mode 100644 gemfiles/rails81_rack31.gemfile create mode 100644 gemfiles/rails81_rack32.gemfile create mode 100644 src/spec/stub/rails81/app/controllers/application_controller.rb create mode 100644 src/spec/stub/rails81/app/helpers/application_helper.rb create mode 100644 src/spec/stub/rails81/config/application.rb create mode 100644 src/spec/stub/rails81/config/boot.rb create mode 100644 src/spec/stub/rails81/config/credentials.yml.enc create mode 100644 src/spec/stub/rails81/config/environment.rb create mode 100644 src/spec/stub/rails81/config/environments/development.rb create mode 100644 src/spec/stub/rails81/config/environments/production.rb create mode 100644 src/spec/stub/rails81/config/environments/test.rb create mode 100644 src/spec/stub/rails81/config/initializers/content_security_policy.rb create mode 100644 src/spec/stub/rails81/config/initializers/filter_parameter_logging.rb create mode 100644 src/spec/stub/rails81/config/initializers/inflections.rb create mode 100644 src/spec/stub/rails81/config/locales/en.yml create mode 100644 src/spec/stub/rails81/config/master.key create mode 100644 src/spec/stub/rails81/config/routes.rb create mode 100644 src/spec/stub/rails81/public/robots.txt diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 1b9eadc08..25983c11b 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -21,7 +21,7 @@ jobs: matrix: jruby_version: [ '10.0.6.0', '10.1.1.0' ] java_version: [ '21', '25' ] - rack_version: [ '~> 2.2.0' ] + rack_version: [ '~> 2.2.0', '~> 3.1.0', '~> 3.2.0' ] fail-fast: false steps: @@ -52,8 +52,9 @@ jobs: strategy: matrix: appraisal: [ - 'rails72_rack22', - 'rails80_rack22', + 'rails72_rack22', 'rails72_rack31', + 'rails80_rack22', 'rails80_rack31', 'rails80_rack32', + 'rails81_rack31', 'rails81_rack32', ] jruby_version: [ '10.0.6.0', '10.1.1.0' ] java_version: [ '21', '25' ] diff --git a/Appraisals b/Appraisals index 8d5d47fea..ae07bc5cf 100644 --- a/Appraisals +++ b/Appraisals @@ -4,8 +4,9 @@ version_spec = ->(prefix, desc) { "~> #{major_minor.call(prefix, desc)}.0" } # Rails version -> rack versions in format # rails#{MAJOR}#{MINOR} => %w[ rack#{MAJOR}#{MINOR} ] { - "rails72" => {racks: %w[rack22]}, - "rails80" => {racks: %w[rack22]} + "rails72" => {racks: %w[rack22 rack31]}, + "rails80" => {racks: %w[rack22 rack31 rack32]}, + "rails81" => {racks: %w[rack31 rack32]} }.each do |rails_desc, c| c[:racks].each do |rack_desc| diff --git a/CHANGELOG.md b/CHANGELOG.md index 2cd4136b0..0337dc9e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ ## 1.3.1 (UNRELEASED) +- feat: support Rack 3.0 -> 3.2 + - `org.jruby.rack.RackEnvironment` gained a `getProtocol()` method (backing the Rack 3.x required `SERVER_PROTOCOL` + Rack env key); custom implementations not extending the servlet request wrapper need to implement it - fix: correct opt-in `ServletEnv` charset mismapping when parsing query strings - fix: ensure `rack.` internal headers are stripped in responses - chore: remove ancient dead Rails 2-era adapter code @@ -8,6 +11,8 @@ - fix: close the original body when ShowStatus replaces it - fix: detect Transfer-Encoding/Content-Length headers case-insensitively - chore: revert `rack.version` value to be Rack 2.2 spec conformant +- fix: handle Array response header values (Rack 3.x) for special-cased headers +- fix: join repeated request header values instead of only passing the first ## 1.3.0 @@ -17,7 +22,6 @@ For most users this should be a minor upgrade; as long as you do not depend on functionality deprecated within JRuby-Rack 1.2.x, EOL JRuby or EOL Rails versions. - Breaking compatibility changes - Drop support for JRuby 9.x (and thus Java < 21) - Drop support for Rails < 7.2 diff --git a/README.md b/README.md index e5a390bdb..b91ba897b 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ For more information on Rack, visit http://rack.github.io/. | JRuby-Rack Series | Status | Rack | JRuby | Java | Rails | Servlet API (min → mostly supported) | Notes | |----------------------------------------------------------------|---------------|-----------|-------------|------|-----------|--------------------------------------|-----------------------------------------------------------| -| [**1.3**](https://github.com/jruby/jruby-rack/tree/1.3-stable) | Maintained | 2.2 | 10.0 → 10.1 | 21+ | 7.2 → 8.0 | 4.0 (Java EE 8) | ✅ _Unofficial_: Rails 6.1 → 7.1 untested, but likely OK. | +| [**1.3**](https://github.com/jruby/jruby-rack/tree/1.3-stable) | Maintained | 2.2 → 3.2 | 10.0 → 10.1 | 21+ | 7.2 → 8.1 | 4.0 (Java EE 8) | ✅ _Unofficial_: Rails 6.1 → 7.1 untested, but likely OK. | | [**1.2**](https://github.com/jruby/jruby-rack/tree/1.2-stable) | EOL @ 2026-09 | 2.2 | 9.3 → 10.1 | 8+ | 5.0 → 8.0 | 3.0 → 4.0 (Java EE 6 → 7) | | | [**1.1**](https://github.com/jruby/jruby-rack/tree/1.1-stable) | EOL @ 2024-05 | 1.x → 2.2 | 1.6 → 9.4 | 6+ | 2.1 → 5.2 | 2.5 → 4.0 (Java EE 5 → 7) | | | [**1.0**](https://github.com/jruby/jruby-rack/tree/1.0.10) | EOL @ 2011-11 | 0.9 → 1.x | 1.1 → 1.9 | 5+ | 2.1 → 3.x | 2.5 (Java EE 5) | | @@ -169,6 +169,9 @@ a filter, the servlet class name is `org.jruby.rack.RackServlet`. - servlet sessions can be used as a (java) session store for Rails, session attributes with String keys (and String, numeric, boolean, or java object values) are automatically copied to the servlet session for you. + Note: on Rack 3.x the servlet session store relies on the `rack-session` + gem - Rails depends on it transitively, but plain Rack applications + configuring `java_servlet_store` need to add it to their Gemfile. ## Rails diff --git a/Rakefile b/Rakefile index b298734ad..d355c0a72 100644 --- a/Rakefile +++ b/Rakefile @@ -143,7 +143,7 @@ task :gem => [:clean, target_jar, target_jruby_rack, target_jruby_rack_version] gem.files = FileList["./**/*"].exclude("*.gem").map{ |f| f.sub(/^\.\//, '') } gem.homepage = %q{http://jruby.org} gem.required_ruby_version = '>= 3.4.0' # JRuby >= 10.0 - gem.add_dependency 'rack', '~> 2.2.0' + gem.add_dependency 'rack', '>= 2.2.0', '< 4' end require 'rubygems/package' diff --git a/gemfiles/rails72_rack31.gemfile b/gemfiles/rails72_rack31.gemfile new file mode 100644 index 000000000..140daa196 --- /dev/null +++ b/gemfiles/rails72_rack31.gemfile @@ -0,0 +1,19 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +group :default do + gem "rack", "~> 3.1.0" + gem "rails", "~> 7.2.0" +end + +group :development do + gem "appraisal", require: nil + gem "rexml" +end + +group :test do + gem "rake", "~> 13.4", require: nil + gem "rspec" + gem "logger" +end diff --git a/gemfiles/rails80_rack31.gemfile b/gemfiles/rails80_rack31.gemfile new file mode 100644 index 000000000..c1e12eecd --- /dev/null +++ b/gemfiles/rails80_rack31.gemfile @@ -0,0 +1,19 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +group :default do + gem "rack", "~> 3.1.0" + gem "rails", "~> 8.0.0" +end + +group :development do + gem "appraisal", require: nil + gem "rexml" +end + +group :test do + gem "rake", "~> 13.4", require: nil + gem "rspec" + gem "logger" +end diff --git a/gemfiles/rails80_rack32.gemfile b/gemfiles/rails80_rack32.gemfile new file mode 100644 index 000000000..a18badf92 --- /dev/null +++ b/gemfiles/rails80_rack32.gemfile @@ -0,0 +1,19 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +group :default do + gem "rack", "~> 3.2.0" + gem "rails", "~> 8.0.0" +end + +group :development do + gem "appraisal", require: nil + gem "rexml" +end + +group :test do + gem "rake", "~> 13.4", require: nil + gem "rspec" + gem "logger" +end diff --git a/gemfiles/rails81_rack31.gemfile b/gemfiles/rails81_rack31.gemfile new file mode 100644 index 000000000..bf570cf1c --- /dev/null +++ b/gemfiles/rails81_rack31.gemfile @@ -0,0 +1,19 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +group :default do + gem "rack", "~> 3.1.0" + gem "rails", "~> 8.1.0" +end + +group :development do + gem "appraisal", require: nil + gem "rexml" +end + +group :test do + gem "rake", "~> 13.4", require: nil + gem "rspec" + gem "logger" +end diff --git a/gemfiles/rails81_rack32.gemfile b/gemfiles/rails81_rack32.gemfile new file mode 100644 index 000000000..9543e219b --- /dev/null +++ b/gemfiles/rails81_rack32.gemfile @@ -0,0 +1,19 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +group :default do + gem "rack", "~> 3.2.0" + gem "rails", "~> 8.1.0" +end + +group :development do + gem "appraisal", require: nil + gem "rexml" +end + +group :test do + gem "rake", "~> 13.4", require: nil + gem "rspec" + gem "logger" +end diff --git a/src/main/java/org/jruby/rack/RackEnvironment.java b/src/main/java/org/jruby/rack/RackEnvironment.java index 7023ccd66..d41e98600 100644 --- a/src/main/java/org/jruby/rack/RackEnvironment.java +++ b/src/main/java/org/jruby/rack/RackEnvironment.java @@ -156,4 +156,10 @@ public interface RackEnvironment { * @return the remote user */ String getRemoteUser(); + + /** + * @see javax.servlet.http.HttpServletRequest#getProtocol() + * @return a String containing the name of the scheme used to make this request + */ + String getProtocol(); } diff --git a/src/main/ruby/jruby/rack/chunked.rb b/src/main/ruby/jruby/rack/chunked.rb index 74c79b858..666905691 100644 --- a/src/main/ruby/jruby/rack/chunked.rb +++ b/src/main/ruby/jruby/rack/chunked.rb @@ -3,21 +3,23 @@ # See the file LICENSE.txt for details. #++ -require 'rack/chunked' # exists since Rack 1.1 +if Rack.release < '3' + require 'rack/chunked' # exists since Rack 1.1, removed in Rack 3.0 -# Disables the Rack response body chunking performed by `Rack::Chunked::Body`. -# It is "necessary" since Rails does instantiate the body directly instead of -# using `Rack::Chunked` as a middleware. -# -# @note This monkey-patch is not required to support chunking with servlets and -# won't be applied unless **jruby.rack.response.dechunk** is 'patch' (default). -# Set **jruby.rack.response.dechunk** to 'true' to simply "dechunk" the body and -# keep the `Rack::Chunked::Body` class as is (or 'false' to do no de-chunking at -# all). -Rack::Chunked::Body.class_eval do + # Disables the Rack response body chunking performed by `Rack::Chunked::Body`. + # It is "necessary" since Rails does instantiate the body directly instead of + # using `Rack::Chunked` as a middleware. + # + # @note This monkey-patch is not required to support chunking with servlets and + # won't be applied unless **jruby.rack.response.dechunk** is 'patch' (default). + # Set **jruby.rack.response.dechunk** to 'true' to simply "dechunk" the body and + # keep the `Rack::Chunked::Body` class as is (or 'false' to do no de-chunking at + # all). + Rack::Chunked::Body.class_eval do + + def each(&block) + @body.each(&block) # no-chunking on servlets + end - def each(&block) - @body.each(&block) # no-chunking on servlets end - end \ No newline at end of file diff --git a/src/main/ruby/jruby/rack/error_app/show_status.rb b/src/main/ruby/jruby/rack/error_app/show_status.rb index 89e012bae..00a1189e1 100644 --- a/src/main/ruby/jruby/rack/error_app/show_status.rb +++ b/src/main/ruby/jruby/rack/error_app/show_status.rb @@ -15,7 +15,6 @@ def initialize(app) def call(env) status, headers, body = @app.call(env) - headers = ::Rack::Utils::HeaderHash.new(headers) empty = headers['Content-Length'].to_i <= 0 detail = env['rack.showstatus.detail'] diff --git a/src/main/ruby/rack/handler/servlet/default_env.rb b/src/main/ruby/rack/handler/servlet/default_env.rb index 40edc3fc3..f4d00b16e 100644 --- a/src/main/ruby/rack/handler/servlet/default_env.rb +++ b/src/main/ruby/rack/handler/servlet/default_env.rb @@ -7,6 +7,8 @@ #++ require 'rack/handler/servlet' +require 'rack' # Rack.release is needed at class definition time - this file +# is auto-loaded on first use, which is after the application boot loads rack module Rack module Handler @@ -19,15 +21,20 @@ class Servlet # ServletRequest input stream to be not read (e.g. for POSTs). class DefaultEnv < Hash # The environment must be an instance of Hash ! - BUILTINS = %w(rack.version rack.input rack.errors rack.url_scheme - rack.multithread rack.multiprocess rack.run_once rack.hijack? - java.servlet_request java.servlet_response java.servlet_context - jruby.rack.version). - map!(&:freeze) + BUILTINS = Rack.release < '3' ? + # rack 2.2.x + Set.new(%w(rack.version rack.multithread rack.multiprocess rack.run_once + rack.input rack.errors rack.url_scheme rack.hijack? + java.servlet_request java.servlet_response java.servlet_context + jruby.rack.context jruby.rack.version).map!(&:freeze)) : + # rack 3.0 and later + Set.new(%w(rack.input rack.errors rack.url_scheme rack.hijack? + java.servlet_request java.servlet_response java.servlet_context + jruby.rack.context jruby.rack.version).map!(&:freeze)) VARIABLES = %w(CONTENT_TYPE CONTENT_LENGTH PATH_INFO QUERY_STRING REMOTE_ADDR REMOTE_HOST REMOTE_USER REQUEST_METHOD REQUEST_URI - SCRIPT_NAME SERVER_NAME SERVER_PORT SERVER_SOFTWARE). + SCRIPT_NAME SERVER_NAME SERVER_PORT SERVER_SOFTWARE SERVER_PROTOCOL). map!(&:freeze) attr_reader :env @@ -216,6 +223,7 @@ def load_variable(env, key) when 'SCRIPT_NAME' then env[key] = @servlet_env.getScriptName when 'SERVER_NAME' then env[key] = @servlet_env.getServerName || '' when 'SERVER_PORT' then env[key] = @servlet_env.getServerPort.to_s + when 'SERVER_PROTOCOL' then env[key] = @servlet_env.getProtocol when 'SERVER_SOFTWARE' then env[key] = rack_context.getServerInfo else # NOTE: even though we allowed for overrides and loaded all attributes @@ -230,6 +238,8 @@ def load_variable(env, key) end def load_builtin(env, key) + return nil unless BUILTINS.include?(key) + case key when 'rack.version' then env[key] = ::Rack::VERSION when 'rack.multithread' then env[key] = true diff --git a/src/main/ruby/rack/handler/servlet/servlet_env.rb b/src/main/ruby/rack/handler/servlet/servlet_env.rb index 03a2ffd7e..a5a1c6952 100644 --- a/src/main/ruby/rack/handler/servlet/servlet_env.rb +++ b/src/main/ruby/rack/handler/servlet/servlet_env.rb @@ -7,6 +7,8 @@ #++ require 'rack/handler/servlet' +require 'rack' # Rack.release is needed at class definition time - this file +# is auto-loaded on first use, which is after the application boot loads rack module Rack module Handler diff --git a/src/spec/ruby/jruby/rack/booter_spec.rb b/src/spec/ruby/jruby/rack/booter_spec.rb index 86afe2e74..9e5512916 100644 --- a/src/spec/ruby/jruby/rack/booter_spec.rb +++ b/src/spec/ruby/jruby/rack/booter_spec.rb @@ -318,7 +318,7 @@ case name.to_sym when :getRealPath then case args.first - when '/WEB-INF' then File.expand_path('rails30/WEB-INF', STUB_DIR) + when '/WEB-INF' then File.expand_path('rails30/WEB-INF', STUB_DIR) # FIXME: rails 3.0 end when :getContextPath then '/' diff --git a/src/spec/ruby/jruby/rack/integration_spec.rb b/src/spec/ruby/jruby/rack/integration_spec.rb index 4fd45998c..8c69e9b7e 100644 --- a/src/spec/ruby/jruby/rack/integration_spec.rb +++ b/src/spec/ruby/jruby/rack/integration_spec.rb @@ -155,10 +155,10 @@ end after(:all) { restore_rails } - it "loaded rack ~> 2.2.0" do + it "loaded the expected (major.minor) rack version" do @runtime = @rack_factory.getApplication.getRuntime should_eval_as_not_nil "defined?(Rack.release)" - should_eval_as_eql_to "Rack.release.to_s[0, 3]", '2.2' + should_eval_as_eql_to "Rack.release.to_s[0, 3]", expected_rack_major_minor end it "booted with a servlet logger" do @@ -191,6 +191,7 @@ end it "disables rack's chunked support (by default)" do + skip "Only runs on Rack < 3.0" unless Rack.release < '3' @runtime = @rack_factory.getApplication.getRuntime expect_to_have_monkey_patched_chunked end @@ -205,6 +206,10 @@ it_should_behave_like 'a rails app' end + describe 'rails 8.1', lib: :rails81 do + it_should_behave_like 'a rails app' + end + def expect_to_have_monkey_patched_chunked @runtime.evalScriptlet "require 'rack/chunked'" script = %{ @@ -258,6 +263,19 @@ def copy_gemfile Dir.chdir File.join(STUB_DIR, name) end + # The Rack 'major.minor' the test environment was *told* to use, read from + # outside the Ruby process rather than from the loaded Rack - so the assertion + # actually catches the app booting a different Rack than intended. This + # Rails-gated test always runs under an appraisal gemfile whose name encodes + # the Rack version as a `rackMM` token (see Appraisals, which maps e.g. + # rack32 -> "~> 3.2.0"); the CI matrix selects it via BUNDLE_GEMFILE. We take + # that externally-chosen token, independent of the generated gemfile content. + def expected_rack_major_minor + token = ENV.fetch('BUNDLE_GEMFILE')[/rack(\d+)/, 1] + raise "no `rackMM` token in BUNDLE_GEMFILE=#{ENV['BUNDLE_GEMFILE'].inspect}" unless token + token.dup.insert(1, '.') # "32" -> "3.2" + end + ENV_COPY = ENV.to_h def restore_rails diff --git a/src/spec/ruby/rack/handler/servlet_env_parsing_spec.rb b/src/spec/ruby/rack/handler/servlet_env_parsing_spec.rb index be21a70e2..47067d3bb 100644 --- a/src/spec/ruby/rack/handler/servlet_env_parsing_spec.rb +++ b/src/spec/ruby/rack/handler/servlet_env_parsing_spec.rb @@ -53,6 +53,7 @@ def get_params(env_class, query_string, params = []) [ 'hash [k] key', 'a%5Bb%5D=1', [ [ 'a[b]', '1' ] ] ], [ 'deep [k][j] nesting', 'a%5Bb%5D%5Bc%5D=x', [ [ 'a[b][c]', 'x' ] ] ], [ 'hash-in-array a[][b]', 'a%5B%5D%5Bb%5D=1', [ [ 'a[][b]', '1' ] ] ], + [ 'nested hash-in-array', 'book%5Bchapters%5D%5B%5D%5Btitle%5D=first&book%5Bchapters%5D%5B%5D%5Btitle%5D=second', [ [ 'book[chapters][][title]', 'first' ], [ 'book[chapters][][title]', 'second' ] ] ], [ 'numeric-index hash', 'huh%5B1%5D=b&huh%5B0%5D=a', [ [ 'huh[1]', 'b' ], [ 'huh[0]', 'a' ] ] ], [ 'bracket-in-bracket meh[]','foo%5Bmeh%5B%5D%5D=x&foo%5Bmeh%5B%5D%5D=42', [ [ 'foo[meh[]]', 'x' ], [ 'foo[meh[]]', '42' ] ] ], [ 'unbalanced brackets', 'foo]=0&bar[=1&baz_=2&[meh=3', [ [ 'foo]', '0' ], [ 'bar[', '1' ], [ 'baz_', '2' ], [ '[meh', '3' ] ] ], diff --git a/src/spec/ruby/rack/handler/servlet_lint_spec.rb b/src/spec/ruby/rack/handler/servlet_lint_spec.rb index b3394f2ec..369ffd09a 100644 --- a/src/spec/ruby/rack/handler/servlet_lint_spec.rb +++ b/src/spec/ruby/rack/handler/servlet_lint_spec.rb @@ -41,7 +41,11 @@ let(:inner_app) do lambda do |env| env['rack.input'].read # exercises the Lint wrapped input contract - [ 200, { 'Content-Type' => 'text/plain', 'Content-Length' => '2' }, [ 'OK' ] ] + if Rack.release >= '3' + [ 200, { 'content-type' => 'text/plain' }, [ 'OK' ] ] + else + [ 200, { 'Content-Type' => 'text/plain', 'Content-Length' => '2' }, [ 'OK' ] ] + end end end diff --git a/src/spec/ruby/rack/handler/servlet_spec.rb b/src/spec/ruby/rack/handler/servlet_spec.rb index a10b23c72..f128ec8e8 100644 --- a/src/spec/ruby/rack/handler/servlet_spec.rb +++ b/src/spec/ruby/rack/handler/servlet_spec.rb @@ -44,10 +44,10 @@ def _env it "creates a hash with the Rack variables in it" do hash = servlet.create_env(@servlet_env) - expect(hash['rack.version']).to eq Rack::VERSION - expect(hash['rack.multithread']).to eq true - expect(hash['rack.multiprocess']).to eq false - expect(hash['rack.run_once']).to eq false + expect(hash['rack.version']).to eq Rack.release < '3' ? Rack::VERSION : nil + expect(hash['rack.multithread']).to eq Rack.release < '3' ? true : nil + expect(hash['rack.multiprocess']).to eq Rack.release < '3' ? false : nil + expect(hash['rack.run_once']).to eq Rack.release < '3' ? false : nil expect(hash['rack.hijack?']).to eq false end @@ -69,6 +69,7 @@ def _env "SERVER_NAME" => "override", "SERVER_PORT" => 8080, "SERVER_SOFTWARE" => "servy", + "SERVER_PROTOCOL" => "HTTP/2.0", "REMOTE_HOST" => "override", "REMOTE_ADDR" => "192.168.0.1", "REMOTE_USER" => "override" @@ -84,6 +85,7 @@ def _env expect(env["SERVER_NAME"]).to eq "override" expect(env["SERVER_PORT"]).to eq "8080" expect(env["SERVER_SOFTWARE"]).to eq "servy" + expect(env["SERVER_PROTOCOL"]).to eq "HTTP/2.0" expect(env["REMOTE_HOST"]).to eq "override" expect(env["REMOTE_ADDR"]).to eq "192.168.0.1" expect(env["REMOTE_USER"]).to eq "override" @@ -164,6 +166,7 @@ def _env @servlet_request.setQueryString('hello=there') @servlet_request.setServerName('serverhost') @servlet_request.setServerPort(80) + @servlet_request.setProtocol('HTTP/1.1') @servlet_request.setRemoteAddr('127.0.0.1') @servlet_request.setRemoteHost('localhost') @servlet_request.setRemoteUser('admin') @@ -177,6 +180,7 @@ def _env expect(env["QUERY_STRING"]).to eq "hello=there" expect(env["SERVER_NAME"]).to eq "serverhost" expect(env["SERVER_PORT"]).to eq "80" + expect(env["SERVER_PROTOCOL"]).to eq "HTTP/1.1" expect(env["REMOTE_HOST"]).to eq "localhost" expect(env["REMOTE_ADDR"]).to eq "127.0.0.1" expect(env["REMOTE_USER"]).to eq "admin" @@ -194,6 +198,7 @@ def _env @servlet_request.setQueryString('hello=there') @servlet_request.setServerName('serverhost') @servlet_request.setServerPort(80) + @servlet_request.setProtocol('HTTP/1.1') @servlet_request.setRemoteAddr('127.0.0.1') @servlet_request.setRemoteHost('localhost') @servlet_request.setRemoteUser('admin') @@ -205,7 +210,7 @@ def _env end env = servlet.create_env @servlet_env - expect(env["rack.version"]).to eq Rack::VERSION + expect(env["rack.version"]).to eq Rack.release < '3' ? Rack::VERSION : nil expect(env["CONTENT_TYPE"]).to eq "text/html" expect(env["HTTP_HOST"]).to eq "serverhost" expect(env["HTTP_ACCEPT"]).to eq "text/*" @@ -216,6 +221,7 @@ def _env expect(env["QUERY_STRING"]).to eq "hello=there" expect(env["SERVER_NAME"]).to eq "serverhost" expect(env["SERVER_PORT"]).to eq "80" + expect(env["SERVER_PROTOCOL"]).to eq "HTTP/1.1" expect(env["REMOTE_HOST"]).to eq "localhost" expect(env["REMOTE_ADDR"]).to eq "127.0.0.1" expect(env["REMOTE_USER"]).to eq "admin" @@ -455,6 +461,7 @@ def getAttributeNames expect(env.keys).to include('QUERY_STRING') expect(env.keys).to include('SERVER_NAME') expect(env.keys).to include('SERVER_PORT') + expect(env.keys).to include('SERVER_PROTOCOL') expect(env.keys).to include('REMOTE_HOST') expect(env.keys).to include('REMOTE_ADDR') expect(env.keys).to include('REMOTE_USER') @@ -462,12 +469,16 @@ def getAttributeNames expect(env.keys).to include(key) end - expect(env.keys).to include('rack.version') + if Rack.release < '3' + expect(env.keys).to include('rack.version') + expect(env.keys).to include('rack.multithread') + expect(env.keys).to include('rack.multiprocess') + expect(env.keys).to include('rack.run_once') + end + expect(env.keys).to include('rack.input') expect(env.keys).to include('rack.errors') expect(env.keys).to include('rack.url_scheme') - expect(env.keys).to include('rack.multithread') - expect(env.keys).to include('rack.run_once') expect(env.keys).to include('java.servlet_context') expect(env.keys).to include('java.servlet_request') expect(env.keys).to include('java.servlet_response') @@ -494,11 +505,15 @@ def getAttributeNames expect { env['OTHER_METHOD'] }.to_not raise_error expect(env['OTHER_METHOD']).to be nil - expect { env['rack.version'] }.to_not raise_error + if Rack.release < '3' + expect { env['rack.version'] }.to_not raise_error + expect { env['rack.multithread'] }.to_not raise_error + expect { env['rack.multiprocess'] }.to_not raise_error + expect { env['rack.run_once'] }.to_not raise_error + end + expect { env['rack.input'] }.to_not raise_error expect { env['rack.errors'] }.to_not raise_error - expect { env['rack.run_once'] }.to_not raise_error - expect { env['rack.multithread'] }.to_not raise_error expect { env['java.servlet_context'] }.to_not raise_error expect { env['java.servlet_request'] }.to_not raise_error expect { env['java.servlet_response'] }.to_not raise_error @@ -624,17 +639,21 @@ def it_works(env) expect(env['SCRIPT_NAME']).to eql '/main' expect(env['SERVER_NAME']).to eql 'serverhost' expect(env['SERVER_PORT']).to eql '80' + expect(env['SERVER_PROTOCOL']).to eql 'HTTP/1.1' expect(env['OTHER_METHOD']).to be nil Rack::Handler::Servlet::DefaultEnv::VARIABLES.each do |key| expect(env[key]).to_not be(nil), "key: #{key.inspect} nil" end expect(env['rack.url_scheme']).to_not be nil - expect(env['rack.version']).to_not be nil expect(env['jruby.rack.version']).to_not be nil - expect(env['rack.run_once']).to be false - expect(env['rack.multithread']).to be true + if Rack.release < '3' + expect(env['rack.version']).to_not be nil + expect(env['rack.multithread']).to be true + expect(env['rack.multiprocess']).to be false + expect(env['rack.run_once']).to be false + end expect(env['rack.whatever']).to be nil @@ -765,6 +784,7 @@ def servlet.create_env(servlet_env) expect(env.keys).to include('QUERY_STRING') expect(env.keys).to include('SERVER_NAME') expect(env.keys).to include('SERVER_PORT') + expect(env.keys).to include('SERVER_PROTOCOL') expect(env.keys).to include('REMOTE_HOST') expect(env.keys).to include('REMOTE_ADDR') expect(env.keys).to include('REMOTE_USER') @@ -772,12 +792,16 @@ def servlet.create_env(servlet_env) expect(env.keys).to include(key) end - expect(env.keys).to include('rack.version') + if Rack.release < '3' + expect(env.keys).to include('rack.version') + expect(env.keys).to include('rack.multithread') + expect(env.keys).to include('rack.multiprocess') + expect(env.keys).to include('rack.run_once') + end + expect(env.keys).to include('rack.input') expect(env.keys).to include('rack.errors') expect(env.keys).to include('rack.url_scheme') - expect(env.keys).to include('rack.multithread') - expect(env.keys).to include('rack.run_once') expect(env.keys).to include('java.servlet_context') expect(env.keys).to include('java.servlet_request') expect(env.keys).to include('java.servlet_response') @@ -928,6 +952,32 @@ def servlet.create_env(servlet_env) expect(rack_request.content_length).to eq content.size.to_s end + it "has correct hash-in-array params when request input has been read" do + # the Rails nested-attributes form shape, e.g. fields_for with an array; + # used to raise TypeError with the (contorted) Rack 3 params algorithm + skip "Rack 2.x parameter mapping does not support hash-in-array params" if Rack.release < '3' + + content = 'book%5Bchapters%5D%5B%5D%5Btitle%5D=first&book%5Bchapters%5D%5B%5D%5Btitle%5D=second' + servlet_request.setContent content.to_java_bytes + servlet_request.addHeader('CONTENT-TYPE', 'application/x-www-form-urlencoded') + servlet_request.setMethod 'POST' + servlet_request.setContextPath '/home' + servlet_request.setPathInfo '/path' + servlet_request.setRequestURI '/home/path' + # NOTE: assume input stream read but getParameter methods work correctly : + read_input_stream servlet_request.getInputStream + servlet_request.addParameter('book[chapters][][title]', 'first') + servlet_request.addParameter('book[chapters][][title]', 'second') + + env = servlet.create_env(servlet_env) + rack_request = Rack::Request.new(env) + + expect(rack_request.GET).to eq({}) + expect(rack_request.POST).to eq({ + 'book' => { 'chapters' => [ { 'title' => 'first' }, { 'title' => 'second' } ] } + }) + end + it "handles null values in parameter-map (Jetty)" do org.springframework.mock.web.MockHttpServletRequest.class_eval do field_reader :parameters diff --git a/src/spec/stub/rails81/app/controllers/application_controller.rb b/src/spec/stub/rails81/app/controllers/application_controller.rb new file mode 100644 index 000000000..0d95db22b --- /dev/null +++ b/src/spec/stub/rails81/app/controllers/application_controller.rb @@ -0,0 +1,4 @@ +class ApplicationController < ActionController::Base + # Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has. + allow_browser versions: :modern +end diff --git a/src/spec/stub/rails81/app/helpers/application_helper.rb b/src/spec/stub/rails81/app/helpers/application_helper.rb new file mode 100644 index 000000000..de6be7945 --- /dev/null +++ b/src/spec/stub/rails81/app/helpers/application_helper.rb @@ -0,0 +1,2 @@ +module ApplicationHelper +end diff --git a/src/spec/stub/rails81/config/application.rb b/src/spec/stub/rails81/config/application.rb new file mode 100644 index 000000000..bba537d7f --- /dev/null +++ b/src/spec/stub/rails81/config/application.rb @@ -0,0 +1,42 @@ +require_relative "boot" + +require "rails" +# Pick the frameworks you want: +require "active_model/railtie" +# require "active_job/railtie" +# require "active_record/railtie" +# require "active_storage/engine" +require "action_controller/railtie" +# require "action_mailer/railtie" +# require "action_mailbox/engine" +# require "action_text/engine" +require "action_view/railtie" +# require "action_cable/engine" +# require "rails/test_unit/railtie" + +# Require the gems listed in Gemfile, including any gems +# you've limited to :test, :development, or :production. +Bundler.require(*Rails.groups) + +module Rails81 + class Application < Rails::Application + # Initialize configuration defaults for originally generated Rails version. + config.load_defaults 8.1 + + # Please, add to the `ignore` list any other `lib` subdirectories that do + # not contain `.rb` files, or that should not be reloaded or eager loaded. + # Common ones are `templates`, `generators`, or `middleware`, for example. + config.autoload_lib(ignore: %w[assets tasks]) + + # Configuration for the application, engines, and railties goes here. + # + # These settings can be overridden in specific environments using the files + # in config/environments, which are processed later. + # + # config.time_zone = "Central Time (US & Canada)" + # config.eager_load_paths << Rails.root.join("extras") + + # Don't generate system test files. + config.generators.system_tests = nil + end +end diff --git a/src/spec/stub/rails81/config/boot.rb b/src/spec/stub/rails81/config/boot.rb new file mode 100644 index 000000000..282011619 --- /dev/null +++ b/src/spec/stub/rails81/config/boot.rb @@ -0,0 +1,3 @@ +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +require "bundler/setup" # Set up gems listed in the Gemfile. diff --git a/src/spec/stub/rails81/config/credentials.yml.enc b/src/spec/stub/rails81/config/credentials.yml.enc new file mode 100644 index 000000000..e67e3468f --- /dev/null +++ b/src/spec/stub/rails81/config/credentials.yml.enc @@ -0,0 +1 @@ +82NrCVblpkSw3wDVYHqQFkuTm4oS5bQt+rYDUmlei6JtoWvmkz/K1A81ysT3RXIojUAgbe2wo3zrl3dygSEmZCSV78wfDXJpBJi5fkVC84HQBJEBg0/8yFhZtDjvWG53X532RYnSVtAaPtPeqKS9F0uHNT7/G8Gkhfgu8JEg1oT1mlT4nT3VLExtH4QrXGBrvpWbFN6VzRIqFIO7Bk/tR92v6VCoAbl44j61pqEhW6SyDLb2PNGtW3o+Lq6RTTnsS9MOXXh/eNr4PZv97ghxbLSxcgzqXh6qBKsfZTTh30oxHRmv1yf1ur7hlopok1g4DcX5yK3Cul+ttkjYUoKdiSZ8cpZojQEqiMaRtvjVBGspHquqi9OuMtBlQaVxU1z3lGCdYnx1hqQDk6RnkBTE+Y7wbTGrjyQ1urKYXjw4bvu5WEh2n/UEHJsxrmg0v7yZZaOGESnfn+pbyWlbWfbA6fhH8ZguyqgzNiMh1TGU4Tqzvmgf/FAPQVNx--Jp0qmDUTnghki/m1--9Dp76qUD7YblOugS/mioww== \ No newline at end of file diff --git a/src/spec/stub/rails81/config/environment.rb b/src/spec/stub/rails81/config/environment.rb new file mode 100644 index 000000000..cac531577 --- /dev/null +++ b/src/spec/stub/rails81/config/environment.rb @@ -0,0 +1,5 @@ +# Load the Rails application. +require_relative "application" + +# Initialize the Rails application. +Rails.application.initialize! diff --git a/src/spec/stub/rails81/config/environments/development.rb b/src/spec/stub/rails81/config/environments/development.rb new file mode 100644 index 000000000..f68dd3575 --- /dev/null +++ b/src/spec/stub/rails81/config/environments/development.rb @@ -0,0 +1,42 @@ +require "active_support/core_ext/integer/time" + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # Make code changes take effect immediately without server restart. + config.enable_reloading = true + + # Do not eager load code on boot. + config.eager_load = false + + # Show full error reports. + config.consider_all_requests_local = true + + # Enable server timing. + config.server_timing = true + + # Enable/disable Action Controller caching. By default Action Controller caching is disabled. + # Run rails dev:cache to toggle Action Controller caching. + if Rails.root.join("tmp/caching-dev.txt").exist? + config.action_controller.perform_caching = true + config.action_controller.enable_fragment_cache_logging = true + config.public_file_server.headers = { "cache-control" => "public, max-age=#{2.days.to_i}" } + else + config.action_controller.perform_caching = false + end + + # Change to :null_store to avoid any caching. + config.cache_store = :memory_store + + # Print deprecation notices to the Rails logger. + config.active_support.deprecation = :log + + # Raises error for missing translations. + # config.i18n.raise_on_missing_translations = true + + # Annotate rendered view with file names. + config.action_view.annotate_rendered_view_with_filenames = true + + # Raise error when a before_action's only/except options reference missing actions. + config.action_controller.raise_on_missing_callback_actions = true +end diff --git a/src/spec/stub/rails81/config/environments/production.rb b/src/spec/stub/rails81/config/environments/production.rb new file mode 100644 index 000000000..49ed6ab53 --- /dev/null +++ b/src/spec/stub/rails81/config/environments/production.rb @@ -0,0 +1,73 @@ +require "active_support/core_ext/integer/time" + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # Code is not reloaded between requests. + config.enable_reloading = false + + # Eager load code on boot for better performance and memory savings (ignored by Rake tasks). + config.eager_load = true + + # Full error reports are disabled. + config.consider_all_requests_local = false + + # Turn on fragment caching in view templates. + config.action_controller.perform_caching = true + + # Cache digest stamped assets for far-future expiry. + # Short cache for others: robots.txt, sitemap.xml, 404.html, etc. + config.public_file_server.headers = { + "cache-control" => lambda do |path, _| + if path.start_with?("/assets/") + # Files in /assets/ are expected to be fully immutable. + # If the content change the URL too. + "public, immutable, max-age=#{1.year.to_i}" + else + # For anything else we cache for 1 minute. + "public, max-age=#{1.minute.to_i}, stale-while-revalidate=#{5.minutes.to_i}" + end + end + } + + # Enable serving of images, stylesheets, and JavaScripts from an asset server. + # config.asset_host = "http://assets.example.com" + + # Assume all access to the app is happening through a SSL-terminating reverse proxy. + config.assume_ssl = true + + # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. + config.force_ssl = true + + # Skip http-to-https redirect for the default health check endpoint. + # config.ssl_options = { redirect: { exclude: ->(request) { request.path == "/up" } } } + + # Log to STDOUT with the current request id as a default log tag. + config.log_tags = [ :request_id ] + # config.logger = ActiveSupport::TaggedLogging.logger(STDOUT) + + # Change to "debug" to log everything (including potentially personally-identifiable information!). + config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info") + + # Prevent health checks from clogging up the logs. + config.silence_healthcheck_path = "/up" + + # Don't log any deprecations. + config.active_support.report_deprecations = false + + # Replace the default in-process memory cache store with a durable alternative. + # config.cache_store = :mem_cache_store + + # Enable locale fallbacks for I18n (makes lookups for any locale fall back to + # the I18n.default_locale when a translation cannot be found). + config.i18n.fallbacks = true + + # Enable DNS rebinding protection and other `Host` header attacks. + # config.hosts = [ + # "example.com", # Allow requests from example.com + # /.*\.example\.com/ # Allow requests from subdomains like `www.example.com` + # ] + # + # Skip DNS rebinding protection for the default health check endpoint. + # config.host_authorization = { exclude: ->(request) { request.path == "/up" } } +end diff --git a/src/spec/stub/rails81/config/environments/test.rb b/src/spec/stub/rails81/config/environments/test.rb new file mode 100644 index 000000000..14bc29e06 --- /dev/null +++ b/src/spec/stub/rails81/config/environments/test.rb @@ -0,0 +1,42 @@ +# The test environment is used exclusively to run your application's +# test suite. You never need to work with it otherwise. Remember that +# your test database is "scratch space" for the test suite and is wiped +# and recreated between test runs. Don't rely on the data there! + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # While tests run files are not watched, reloading is not necessary. + config.enable_reloading = false + + # Eager loading loads your entire application. When running a single test locally, + # this is usually not necessary, and can slow down your test suite. However, it's + # recommended that you enable it in continuous integration systems to ensure eager + # loading is working properly before deploying your code. + config.eager_load = ENV["CI"].present? + + # Configure public file server for tests with cache-control for performance. + config.public_file_server.headers = { "cache-control" => "public, max-age=3600" } + + # Show full error reports. + config.consider_all_requests_local = true + config.cache_store = :null_store + + # Render exception templates for rescuable exceptions and raise for other exceptions. + config.action_dispatch.show_exceptions = :rescuable + + # Disable request forgery protection in test environment. + config.action_controller.allow_forgery_protection = false + + # Print deprecation notices to the stderr. + config.active_support.deprecation = :stderr + + # Raises error for missing translations. + # config.i18n.raise_on_missing_translations = true + + # Annotate rendered view with file names. + # config.action_view.annotate_rendered_view_with_filenames = true + + # Raise error when a before_action's only/except options reference missing actions. + config.action_controller.raise_on_missing_callback_actions = true +end diff --git a/src/spec/stub/rails81/config/initializers/content_security_policy.rb b/src/spec/stub/rails81/config/initializers/content_security_policy.rb new file mode 100644 index 000000000..d51d71397 --- /dev/null +++ b/src/spec/stub/rails81/config/initializers/content_security_policy.rb @@ -0,0 +1,29 @@ +# Be sure to restart your server when you modify this file. + +# Define an application-wide content security policy. +# See the Securing Rails Applications Guide for more information: +# https://guides.rubyonrails.org/security.html#content-security-policy-header + +# Rails.application.configure do +# config.content_security_policy do |policy| +# policy.default_src :self, :https +# policy.font_src :self, :https, :data +# policy.img_src :self, :https, :data +# policy.object_src :none +# policy.script_src :self, :https +# policy.style_src :self, :https +# # Specify URI for violation reports +# # policy.report_uri "/csp-violation-report-endpoint" +# end +# +# # Generate session nonces for permitted importmap, inline scripts, and inline styles. +# config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } +# config.content_security_policy_nonce_directives = %w(script-src style-src) +# +# # Automatically add `nonce` to `javascript_tag`, `javascript_include_tag`, and `stylesheet_link_tag` +# # if the corresponding directives are specified in `content_security_policy_nonce_directives`. +# # config.content_security_policy_nonce_auto = true +# +# # Report violations without enforcing the policy. +# # config.content_security_policy_report_only = true +# end diff --git a/src/spec/stub/rails81/config/initializers/filter_parameter_logging.rb b/src/spec/stub/rails81/config/initializers/filter_parameter_logging.rb new file mode 100644 index 000000000..c0b717f7e --- /dev/null +++ b/src/spec/stub/rails81/config/initializers/filter_parameter_logging.rb @@ -0,0 +1,8 @@ +# Be sure to restart your server when you modify this file. + +# Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file. +# Use this to limit dissemination of sensitive information. +# See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors. +Rails.application.config.filter_parameters += [ + :passw, :email, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn, :cvv, :cvc +] diff --git a/src/spec/stub/rails81/config/initializers/inflections.rb b/src/spec/stub/rails81/config/initializers/inflections.rb new file mode 100644 index 000000000..3860f659e --- /dev/null +++ b/src/spec/stub/rails81/config/initializers/inflections.rb @@ -0,0 +1,16 @@ +# Be sure to restart your server when you modify this file. + +# Add new inflection rules using the following format. Inflections +# are locale specific, and you may define rules for as many different +# locales as you wish. All of these examples are active by default: +# ActiveSupport::Inflector.inflections(:en) do |inflect| +# inflect.plural /^(ox)$/i, "\\1en" +# inflect.singular /^(ox)en/i, "\\1" +# inflect.irregular "person", "people" +# inflect.uncountable %w( fish sheep ) +# end + +# These inflection rules are supported but not enabled by default: +# ActiveSupport::Inflector.inflections(:en) do |inflect| +# inflect.acronym "RESTful" +# end diff --git a/src/spec/stub/rails81/config/locales/en.yml b/src/spec/stub/rails81/config/locales/en.yml new file mode 100644 index 000000000..6c349ae5e --- /dev/null +++ b/src/spec/stub/rails81/config/locales/en.yml @@ -0,0 +1,31 @@ +# Files in the config/locales directory are used for internationalization and +# are automatically loaded by Rails. If you want to use locales other than +# English, add the necessary files in this directory. +# +# To use the locales, use `I18n.t`: +# +# I18n.t "hello" +# +# In views, this is aliased to just `t`: +# +# <%= t("hello") %> +# +# To use a different locale, set it with `I18n.locale`: +# +# I18n.locale = :es +# +# This would use the information in config/locales/es.yml. +# +# To learn more about the API, please read the Rails Internationalization guide +# at https://guides.rubyonrails.org/i18n.html. +# +# Be aware that YAML interprets the following case-insensitive strings as +# booleans: `true`, `false`, `on`, `off`, `yes`, `no`. Therefore, these strings +# must be quoted to be interpreted as strings. For example: +# +# en: +# "yes": yup +# enabled: "ON" + +en: + hello: "Hello world" diff --git a/src/spec/stub/rails81/config/master.key b/src/spec/stub/rails81/config/master.key new file mode 100644 index 000000000..e7883e7b5 --- /dev/null +++ b/src/spec/stub/rails81/config/master.key @@ -0,0 +1 @@ +c8efa0d2b5305bf19054185dc011f0f7 \ No newline at end of file diff --git a/src/spec/stub/rails81/config/routes.rb b/src/spec/stub/rails81/config/routes.rb new file mode 100644 index 000000000..48254e88e --- /dev/null +++ b/src/spec/stub/rails81/config/routes.rb @@ -0,0 +1,14 @@ +Rails.application.routes.draw do + # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html + + # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. + # Can be used by load balancers and uptime monitors to verify that the app is live. + get "up" => "rails/health#show", as: :rails_health_check + + # Render dynamic PWA files from app/views/pwa/* (remember to link manifest in application.html.erb) + # get "manifest" => "rails/pwa#manifest", as: :pwa_manifest + # get "service-worker" => "rails/pwa#service_worker", as: :pwa_service_worker + + # Defines the root path route ("/") + # root "posts#index" +end diff --git a/src/spec/stub/rails81/public/robots.txt b/src/spec/stub/rails81/public/robots.txt new file mode 100644 index 000000000..c19f78ab6 --- /dev/null +++ b/src/spec/stub/rails81/public/robots.txt @@ -0,0 +1 @@ +# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file From f7a42dcb821371a390978b4c5a58c7412634287f Mon Sep 17 00:00:00 2001 From: Chad Wilson <29788154+chadlwilson@users.noreply.github.com> Date: Tue, 8 Sep 2026 16:52:34 +0800 Subject: [PATCH 02/10] feat: add form_pairs support for Rack 3.2 compat --- src/main/ruby/rack/handler/servlet/servlet_env.rb | 7 +++++++ src/spec/ruby/rack/handler/servlet_spec.rb | 9 +++++++++ 2 files changed, 16 insertions(+) diff --git a/src/main/ruby/rack/handler/servlet/servlet_env.rb b/src/main/ruby/rack/handler/servlet/servlet_env.rb index a5a1c6952..c253a0691 100644 --- a/src/main/ruby/rack/handler/servlet/servlet_env.rb +++ b/src/main/ruby/rack/handler/servlet/servlet_env.rb @@ -49,6 +49,8 @@ def load_env_key(env, key) FORM_INPUT = "rack.request.form_input".freeze # @private FORM_HASH = "rack.request.form_hash".freeze + # @private + FORM_PAIRS = "rack.request.form_pairs".freeze # Rack 3.2+ # @private POST_PARAM_METHODS = [ 'POST', 'PUT', 'DELETE' ].freeze @@ -59,6 +61,7 @@ def load_parameters get_only = ! POST_PARAM_METHODS.include?( @servlet_env.getMethod ) # we only need to really do this for POSTs but we'll handle all query_params, form_params = query_parser.make_params, query_parser.make_params + form_pairs = [] # raw (un-nested) POST name/value pairs for Rack 3.2+ # NOTE: HttpServletRequest#getParameterMap merges query-string and # (POST) body parameters and exposes *every* raw value per name - # including repeated names that do not end with '[]' and names that @@ -84,11 +87,13 @@ def load_parameters end store_parameter(query_params, key, get_vals) store_parameter(form_params, key, post_vals) + post_vals.each { |v| form_pairs << [ key, v ] } else store_parameter(query_params, key, val) end else # POST param : store_parameter(form_params, key, val) + val.each { |v| form_pairs << [ key, v ] } end end # Rack::Request#GET @@ -98,6 +103,8 @@ def load_parameters # TODO should recreate the input e.g. multipart/form-data ... @env[ FORM_INPUT ] = @env['rack.input'] @env[ FORM_HASH ] = form_params.to_h + # Rack::Request#form_pairs (Rack 3.2+, ignored by older Rack) + @env[ FORM_PAIRS ] = form_pairs end def [](key) diff --git a/src/spec/ruby/rack/handler/servlet_spec.rb b/src/spec/ruby/rack/handler/servlet_spec.rb index f128ec8e8..88885c22d 100644 --- a/src/spec/ruby/rack/handler/servlet_spec.rb +++ b/src/spec/ruby/rack/handler/servlet_spec.rb @@ -945,6 +945,15 @@ def servlet.create_env(servlet_env) "name" => ["Ferko Suska", "Jozko Hruska"], "formula" => "a + b == 42%!" }) + if rack_request.respond_to?(:form_pairs) # Rack 3.2+ + # POST name/value pairs, preserving duplicate (raw, un-nested) names, + # available even though the servlet input stream was already consumed + expect(rack_request.form_pairs).to match_array([ + [ 'name[]', 'Ferko Suska' ], [ 'name[]', 'Jozko Hruska' ], + [ 'age', '30' ], [ 'formula', 'a + b == 42%!' ] + ]) + end + expect(rack_request.query_string).to eq 'foo=bad&foo=bar&bar=huu&age=33' expect(rack_request.request_method).to eq 'POST' expect(rack_request.path_info).to eq '/path' From ed0659b8db7d4c5eb1d6f1e5201c2030e0f7e116 Mon Sep 17 00:00:00 2001 From: Chad Wilson <29788154+chadlwilson@users.noreply.github.com> Date: Tue, 28 Oct 2025 00:52:17 +0800 Subject: [PATCH 03/10] chore: Make sinatra example run with Rack 3.2 --- examples/sinatra/Gemfile | 8 ++------ examples/sinatra/lib/env.rb | 1 + examples/sinatra/views/info.erb | 2 +- examples/sinatra/views/jsp_include.erb | 2 +- 4 files changed, 5 insertions(+), 8 deletions(-) diff --git a/examples/sinatra/Gemfile b/examples/sinatra/Gemfile index f77c3478d..e7d5c4c4c 100644 --- a/examples/sinatra/Gemfile +++ b/examples/sinatra/Gemfile @@ -2,12 +2,8 @@ source 'https://rubygems.org' ruby RUBY_VERSION -gem 'sinatra', '< 4' # v4 requires Rack 3.x support - -if JRUBY_VERSION.start_with?('10.1.') - gem 'ostruct' - gem 'logger' -end +gem 'sinatra', '< 5' +gem 'rack', '~> 3.2.0' group :development do gem 'jruby-jars', JRUBY_VERSION diff --git a/examples/sinatra/lib/env.rb b/examples/sinatra/lib/env.rb index d5ac94e92..db1cbc59a 100644 --- a/examples/sinatra/lib/env.rb +++ b/examples/sinatra/lib/env.rb @@ -21,6 +21,7 @@ post '/body' do res = "Content-Type was: #{request.content_type.inspect}\n" + request.body.rewind # Need to rewind to re-read body with Rack 3 - and rewindable bodies are not mandatory... body = request.body.read if body.empty? status 400 diff --git a/examples/sinatra/views/info.erb b/examples/sinatra/views/info.erb index 089be1d91..32289c49b 100644 --- a/examples/sinatra/views/info.erb +++ b/examples/sinatra/views/info.erb @@ -1,4 +1,4 @@ -rack.version: <%= env["rack.version"].inspect %> +jruby.rack.version: <%= env["jruby.rack.version"].inspect %> CONTENT_TYPE: <%= env["CONTENT_TYPE"].inspect %> HTTP_HOST: <%= env["HTTP_HOST"].inspect %> HTTP_ACCEPT: <%= env["HTTP_ACCEPT"].inspect %> diff --git a/examples/sinatra/views/jsp_include.erb b/examples/sinatra/views/jsp_include.erb index 3e945476b..03a5e83d7 100644 --- a/examples/sinatra/views/jsp_include.erb +++ b/examples/sinatra/views/jsp_include.erb @@ -7,7 +7,7 @@