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..52483dca8 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,9 @@ - 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 +- fix: do not mutate (potentially frozen) response header values when writing ## 1.3.0 @@ -17,7 +23,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/examples/README.md b/examples/README.md index 4d21a5605..e5366f828 100644 --- a/examples/README.md +++ b/examples/README.md @@ -26,24 +26,25 @@ As an executable jar within Jetty: ## Demo routes -| Example | Component | Embedded Route | Deployed War Route | -|---------|------------------------|-------------------------------------|---------------------------------------------| -| Rails 7 | Status Page | http://localhost:8080/up | http://localhost:8080/rails7/up | -| Rails 7 | Snoop Dump | http://localhost:8989/snoop | http://localhost:8080/rails7/snoop | -| Rails 7 | Simple Form submission | http://localhost:8989/simple_form | http://localhost:8080/rails7/simple_form | -| Rails 7 | Body Posts | http://localhost:8989/body | http://localhost:8080/rails7/body | -| Rails 7 | JSP (render) | http://localhost:8989/jsp/ | http://localhost:8080/rails7/jsp/ | -| Rails 7 | JSP (forward to) | http://localhost:8989/jsp-forward/ | http://localhost:8080/rails7/jsp-forward/ | -| Rails 7 | JSP (include) | http://localhost:8989/jsp-include/ | http://localhost:8080/rails7/jsp-include/ | -| Sinatra | Demo Index | http://localhost:8989/ | http://localhost:8080/sinatra | -| Sinatra | Info | http://localhost:8989/info | http://localhost:8080/sinatra/info | -| Sinatra | Snoop Dump | http://localhost:8989/env | http://localhost:8080/sinatra/env | -| Sinatra | JSP (render) | http://localhost:8989/jsp/index.jsp | http://localhost:8080/sinatra/jsp/index.jsp | -| Sinatra | JSP (forward to) | http://localhost:8989/jsp_forward | http://localhost:8080/sinatra/jsp_forward | -| Sinatra | JSP (include) | http://localhost:8989/jsp_include | http://localhost:8080/sinatra/jsp_include | -| Sinatra | Streaming Demo | http://localhost:8989/stream | http://localhost:8080/sinatra/stream | -| Camping | Demo Index | http://localhost:8989/ | http://localhost:8080/camping | -| Camping | Snoop Dump | http://localhost:8989/snoop | http://localhost:8080/camping/snoop | +| Example | Component | Embedded Route | Deployed War Route | +|---------|---------------------------|-------------------------------------|---------------------------------------------| +| Rails 8 | Status Page | http://localhost:8080/up | http://localhost:8080/rails8/up | +| Rails 8 | Snoop Dump | http://localhost:8989/snoop | http://localhost:8080/rails8/snoop | +| Rails 8 | Simple Form submission | http://localhost:8989/simple_form | http://localhost:8080/rails8/simple_form | +| Rails 8 | Body Posts | http://localhost:8989/body | http://localhost:8080/rails8/body | +| Rails 8 | JSP (render) | http://localhost:8989/jsp/ | http://localhost:8080/rails8/jsp/ | +| Rails 8 | JSP (forward to) | http://localhost:8989/jsp-forward/ | http://localhost:8080/rails8/jsp-forward/ | +| Rails 8 | JSP (include) | http://localhost:8989/jsp-include/ | http://localhost:8080/rails8/jsp-include/ | +| Sinatra | Demo Index | http://localhost:8989/ | http://localhost:8080/sinatra | +| Sinatra | Info | http://localhost:8989/info | http://localhost:8080/sinatra/info | +| Sinatra | Snoop Dump | http://localhost:8989/env | http://localhost:8080/sinatra/env | +| Sinatra | JSP (render) | http://localhost:8989/jsp/index.jsp | http://localhost:8080/sinatra/jsp/index.jsp | +| Sinatra | JSP (forward to) | http://localhost:8989/jsp_forward | http://localhost:8080/sinatra/jsp_forward | +| Sinatra | JSP (include) | http://localhost:8989/jsp_include | http://localhost:8080/sinatra/jsp_include | +| Sinatra | Streaming Enumerable Demo | http://localhost:8989/stream | http://localhost:8080/sinatra/stream | +| Sinatra | Streaming Proc Demo | http://localhost:8989/stream_call | http://localhost:8080/sinatra/stream_call | +| Camping | Demo Index | http://localhost:8989/ | http://localhost:8080/camping | +| Camping | Snoop Dump | http://localhost:8989/snoop | http://localhost:8080/camping/snoop | ## Development diff --git a/examples/camping/Gemfile b/examples/camping/Gemfile index d196905b7..c7f6393ab 100644 --- a/examples/camping/Gemfile +++ b/examples/camping/Gemfile @@ -2,7 +2,8 @@ source 'https://rubygems.org' ruby RUBY_VERSION -gem 'camping', '< 3' # v3 requires Rack 3.x support +gem 'camping', '< 4' +gem 'rack', '~> 3.2.0' group :development do gem 'jruby-jars', JRUBY_VERSION diff --git a/examples/camping/lib/demo.rb b/examples/camping/lib/demo.rb index 1f9d94894..f52883d0e 100644 --- a/examples/camping/lib/demo.rb +++ b/examples/camping/lib/demo.rb @@ -55,3 +55,5 @@ def snoop div { dl_hash(@snoop) } end end + +Camping.make_camp diff --git a/examples/rails7/config/environments/production.rb b/examples/rails7/config/environments/production.rb deleted file mode 100644 index 1949e9d1d..000000000 --- a/examples/rails7/config/environments/production.rb +++ /dev/null @@ -1,79 +0,0 @@ -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. This eager loads most of Rails and - # your application in memory, allowing both threaded web servers - # and those relying on copy on write to perform better. - # Rake tasks automatically ignore this option for performance. - config.eager_load = true - - # Full error reports are disabled and caching is turned on. - config.consider_all_requests_local = false - config.action_controller.perform_caching = true - - # Ensures that a master key has been made available in ENV["RAILS_MASTER_KEY"], config/master.key, or an environment - # key such as config/credentials/production.key. This key is used to decrypt credentials (and other encrypted files). - # config.require_master_key = true - - # Disable serving static files from `public/`, relying on NGINX/Apache to do so instead. - # config.public_file_server.enabled = false - - # Compress CSS using a preprocessor. - # config.assets.css_compressor = :sass - - # Fall back to assets pipeline if a precompiled asset is missed. - config.assets.compile = true - - # Enable serving of images, stylesheets, and JavaScripts from an asset server. - # config.asset_host = "http://assets.example.com" - - # Specifies the header that your server uses for sending files. - # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache - # config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for NGINX - - # Assume all access to the app is happening through a SSL-terminating reverse proxy. - # Can be used together with config.force_ssl for Strict-Transport-Security and secure cookies. - # config.assume_ssl = true - - # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. - config.force_ssl = false - - # Skip http-to-https redirect for the default health check endpoint. - # config.ssl_options = { redirect: { exclude: ->(request) { request.path == "/up" } } } - - # Log to STDOUT by default - config.logger = ActiveSupport::Logger.new(STDOUT) - .tap { |logger| logger.formatter = ::Logger::Formatter.new } - .then { |logger| ActiveSupport::TaggedLogging.new(logger) } - - # Prepend all log lines with the following tags. - config.log_tags = [ :request_id ] - - # "info" includes generic and useful information about system operation, but avoids logging too much - # information to avoid inadvertent exposure of personally identifiable information (PII). If you - # want to log everything, set the level to "debug". - config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info") - - # Use a different cache store in production. - # 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 - - # Don't log any deprecations. - config.active_support.report_deprecations = false - - # 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/examples/rails7/config/initializers/permissions_policy.rb b/examples/rails7/config/initializers/permissions_policy.rb deleted file mode 100644 index 7db3b9577..000000000 --- a/examples/rails7/config/initializers/permissions_policy.rb +++ /dev/null @@ -1,13 +0,0 @@ -# Be sure to restart your server when you modify this file. - -# Define an application-wide HTTP permissions policy. For further -# information see: https://developers.google.com/web/updates/2018/06/feature-policy - -# Rails.application.config.permissions_policy do |policy| -# policy.camera :none -# policy.gyroscope :none -# policy.microphone :none -# policy.usb :none -# policy.fullscreen :self -# policy.payment :self, "https://secure.example.com" -# end diff --git a/examples/rails7/config/master.key b/examples/rails7/config/master.key deleted file mode 100644 index a73c9f66c..000000000 --- a/examples/rails7/config/master.key +++ /dev/null @@ -1 +0,0 @@ -1bf39111dcca7a9dd0f154a78f40624c \ No newline at end of file diff --git a/examples/rails7/config/puma.rb b/examples/rails7/config/puma.rb deleted file mode 100644 index 03c166f4c..000000000 --- a/examples/rails7/config/puma.rb +++ /dev/null @@ -1,34 +0,0 @@ -# This configuration file will be evaluated by Puma. The top-level methods that -# are invoked here are part of Puma's configuration DSL. For more information -# about methods provided by the DSL, see https://puma.io/puma/Puma/DSL.html. - -# Puma starts a configurable number of processes (workers) and each process -# serves each request in a thread from an internal thread pool. -# -# The ideal number of threads per worker depends both on how much time the -# application spends waiting for IO operations and on how much you wish to -# to prioritize throughput over latency. -# -# As a rule of thumb, increasing the number of threads will increase how much -# traffic a given process can handle (throughput), but due to CRuby's -# Global VM Lock (GVL) it has diminishing returns and will degrade the -# response time (latency) of the application. -# -# The default is set to 3 threads as it's deemed a decent compromise between -# throughput and latency for the average Rails application. -# -# Any libraries that use a connection pool or another resource pool should -# be configured to provide at least as many connections as the number of -# threads. This includes Active Record's `pool` parameter in `database.yml`. -threads_count = ENV.fetch("RAILS_MAX_THREADS", 3) -threads threads_count, threads_count - -# Specifies the `port` that Puma will listen on to receive requests; default is 3000. -port ENV.fetch("PORT", 3000) - -# Allow puma to be restarted by `bin/rails restart` command. -plugin :tmp_restart - -# Specify the PID file. Defaults to tmp/pids/server.pid in development. -# In other environments, only set the PID file if requested. -pidfile ENV["PIDFILE"] if ENV["PIDFILE"] diff --git a/examples/rails7/.bundle/config b/examples/rails8/.bundle/config similarity index 100% rename from examples/rails7/.bundle/config rename to examples/rails8/.bundle/config diff --git a/examples/rails7/.gitignore b/examples/rails8/.gitignore similarity index 100% rename from examples/rails7/.gitignore rename to examples/rails8/.gitignore diff --git a/examples/rails7/Gemfile b/examples/rails8/Gemfile similarity index 65% rename from examples/rails7/Gemfile rename to examples/rails8/Gemfile index f6c1746eb..75bd85e40 100644 --- a/examples/rails7/Gemfile +++ b/examples/rails8/Gemfile @@ -2,8 +2,10 @@ source 'https://rubygems.org' ruby RUBY_VERSION -gem 'rails', '~> 7.2.0' +gem 'rails', '~> 8.1.0' +gem 'rack', '~> 3.2.0' gem 'sprockets-rails' +gem 'json', '< 3' # ActiveSupport 8.1 not compatible with json 3 yet. group :development do gem 'jruby-jars', JRUBY_VERSION diff --git a/examples/rails7/Rakefile b/examples/rails8/Rakefile similarity index 100% rename from examples/rails7/Rakefile rename to examples/rails8/Rakefile diff --git a/examples/rails7/app/assets/config/manifest.js b/examples/rails8/app/assets/config/manifest.js similarity index 100% rename from examples/rails7/app/assets/config/manifest.js rename to examples/rails8/app/assets/config/manifest.js diff --git a/examples/rails7/app/assets/images/rails.png b/examples/rails8/app/assets/images/rails.png similarity index 100% rename from examples/rails7/app/assets/images/rails.png rename to examples/rails8/app/assets/images/rails.png diff --git a/examples/rails7/app/assets/stylesheets/application.css b/examples/rails8/app/assets/stylesheets/application.css similarity index 100% rename from examples/rails7/app/assets/stylesheets/application.css rename to examples/rails8/app/assets/stylesheets/application.css diff --git a/examples/rails7/app/controllers/application_controller.rb b/examples/rails8/app/controllers/application_controller.rb similarity index 100% rename from examples/rails7/app/controllers/application_controller.rb rename to examples/rails8/app/controllers/application_controller.rb diff --git a/examples/rails7/app/controllers/body_controller.rb b/examples/rails8/app/controllers/body_controller.rb similarity index 74% rename from examples/rails7/app/controllers/body_controller.rb rename to examples/rails8/app/controllers/body_controller.rb index 620fc9fc0..5a7416d7e 100644 --- a/examples/rails7/app/controllers/body_controller.rb +++ b/examples/rails8/app/controllers/body_controller.rb @@ -9,6 +9,7 @@ def index private def body_size bytes = 0 + request.body.rewind # Need to rewind to re-read body with Rack 3 - and rewindable bodies are not mandatory... while str = request.body.read(1024) bytes += str.size end diff --git a/examples/rails7/app/controllers/cache_headers_controller.rb b/examples/rails8/app/controllers/cache_headers_controller.rb similarity index 100% rename from examples/rails7/app/controllers/cache_headers_controller.rb rename to examples/rails8/app/controllers/cache_headers_controller.rb diff --git a/examples/rails7/app/controllers/images_controller.rb b/examples/rails8/app/controllers/images_controller.rb similarity index 100% rename from examples/rails7/app/controllers/images_controller.rb rename to examples/rails8/app/controllers/images_controller.rb diff --git a/examples/rails7/app/controllers/jsp_controller.rb b/examples/rails8/app/controllers/jsp_controller.rb similarity index 100% rename from examples/rails7/app/controllers/jsp_controller.rb rename to examples/rails8/app/controllers/jsp_controller.rb diff --git a/examples/rails7/app/controllers/jsp_forward_controller.rb b/examples/rails8/app/controllers/jsp_forward_controller.rb similarity index 100% rename from examples/rails7/app/controllers/jsp_forward_controller.rb rename to examples/rails8/app/controllers/jsp_forward_controller.rb diff --git a/examples/rails7/app/controllers/jsp_include_controller.rb b/examples/rails8/app/controllers/jsp_include_controller.rb similarity index 100% rename from examples/rails7/app/controllers/jsp_include_controller.rb rename to examples/rails8/app/controllers/jsp_include_controller.rb diff --git a/examples/rails7/app/controllers/simple_form_controller.rb b/examples/rails8/app/controllers/simple_form_controller.rb similarity index 100% rename from examples/rails7/app/controllers/simple_form_controller.rb rename to examples/rails8/app/controllers/simple_form_controller.rb diff --git a/examples/rails7/app/controllers/snoop_controller.rb b/examples/rails8/app/controllers/snoop_controller.rb similarity index 100% rename from examples/rails7/app/controllers/snoop_controller.rb rename to examples/rails8/app/controllers/snoop_controller.rb diff --git a/examples/rails7/app/helpers/application_helper.rb b/examples/rails8/app/helpers/application_helper.rb similarity index 100% rename from examples/rails7/app/helpers/application_helper.rb rename to examples/rails8/app/helpers/application_helper.rb diff --git a/examples/rails7/app/helpers/assets_helper.rb b/examples/rails8/app/helpers/assets_helper.rb similarity index 100% rename from examples/rails7/app/helpers/assets_helper.rb rename to examples/rails8/app/helpers/assets_helper.rb diff --git a/examples/rails7/app/helpers/cache_headers_helper.rb b/examples/rails8/app/helpers/cache_headers_helper.rb similarity index 100% rename from examples/rails7/app/helpers/cache_headers_helper.rb rename to examples/rails8/app/helpers/cache_headers_helper.rb diff --git a/examples/rails7/app/helpers/queue_helper.rb b/examples/rails8/app/helpers/queue_helper.rb similarity index 100% rename from examples/rails7/app/helpers/queue_helper.rb rename to examples/rails8/app/helpers/queue_helper.rb diff --git a/examples/rails7/app/helpers/snoop_helper.rb b/examples/rails8/app/helpers/snoop_helper.rb similarity index 100% rename from examples/rails7/app/helpers/snoop_helper.rb rename to examples/rails8/app/helpers/snoop_helper.rb diff --git a/examples/rails7/app/views/body/index.html.erb b/examples/rails8/app/views/body/index.html.erb similarity index 100% rename from examples/rails7/app/views/body/index.html.erb rename to examples/rails8/app/views/body/index.html.erb diff --git a/examples/rails7/app/views/images/show.html.erb b/examples/rails8/app/views/images/show.html.erb similarity index 100% rename from examples/rails7/app/views/images/show.html.erb rename to examples/rails8/app/views/images/show.html.erb diff --git a/examples/rails7/app/views/jsp_include/index.html.erb b/examples/rails8/app/views/jsp_include/index.html.erb similarity index 100% rename from examples/rails7/app/views/jsp_include/index.html.erb rename to examples/rails8/app/views/jsp_include/index.html.erb diff --git a/examples/rails7/app/views/layouts/application.html.erb b/examples/rails8/app/views/layouts/application.html.erb similarity index 100% rename from examples/rails7/app/views/layouts/application.html.erb rename to examples/rails8/app/views/layouts/application.html.erb diff --git a/examples/rails7/app/views/simple_form/index.html.erb b/examples/rails8/app/views/simple_form/index.html.erb similarity index 100% rename from examples/rails7/app/views/simple_form/index.html.erb rename to examples/rails8/app/views/simple_form/index.html.erb diff --git a/examples/rails7/app/views/snoop/index.html.erb b/examples/rails8/app/views/snoop/index.html.erb similarity index 100% rename from examples/rails7/app/views/snoop/index.html.erb rename to examples/rails8/app/views/snoop/index.html.erb diff --git a/examples/rails7/app/views/snoop/session_form.html.erb b/examples/rails8/app/views/snoop/session_form.html.erb similarity index 100% rename from examples/rails7/app/views/snoop/session_form.html.erb rename to examples/rails8/app/views/snoop/session_form.html.erb diff --git a/examples/rails7/config.ru b/examples/rails8/config.ru similarity index 100% rename from examples/rails7/config.ru rename to examples/rails8/config.ru diff --git a/examples/rails7/config/application.rb b/examples/rails8/config/application.rb similarity index 96% rename from examples/rails7/config/application.rb rename to examples/rails8/config/application.rb index 5e4be1049..bba537d7f 100644 --- a/examples/rails7/config/application.rb +++ b/examples/rails8/config/application.rb @@ -18,10 +18,10 @@ # you've limited to :test, :development, or :production. Bundler.require(*Rails.groups) -module Rails7 +module Rails81 class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. - config.load_defaults 7.2 + 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. diff --git a/examples/rails7/config/boot.rb b/examples/rails8/config/boot.rb similarity index 100% rename from examples/rails7/config/boot.rb rename to examples/rails8/config/boot.rb diff --git a/examples/rails7/config/credentials.yml.enc b/examples/rails8/config/credentials.yml.enc similarity index 100% rename from examples/rails7/config/credentials.yml.enc rename to examples/rails8/config/credentials.yml.enc diff --git a/examples/rails7/config/environment.rb b/examples/rails8/config/environment.rb similarity index 100% rename from examples/rails7/config/environment.rb rename to examples/rails8/config/environment.rb diff --git a/examples/rails7/config/environments/development.rb b/examples/rails8/config/environments/development.rb similarity index 58% rename from examples/rails7/config/environments/development.rb rename to examples/rails8/config/environments/development.rb index d6ec4a787..f68dd3575 100644 --- a/examples/rails7/config/environments/development.rb +++ b/examples/rails8/config/environments/development.rb @@ -3,9 +3,7 @@ Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. - # In the development environment your application's code is reloaded any time - # it changes. This slows down response time but is perfect for development - # since you don't have to restart the web server when you make code changes. + # Make code changes take effect immediately without server restart. config.enable_reloading = true # Do not eager load code on boot. @@ -17,32 +15,22 @@ # Enable server timing. config.server_timing = true - # Enable/disable caching. By default caching is disabled. - # Run rails dev:cache to toggle caching. + # 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.cache_store = :memory_store - config.public_file_server.headers = { "Cache-Control" => "public, max-age=#{2.days.to_i}" } + config.public_file_server.headers = { "cache-control" => "public, max-age=#{2.days.to_i}" } else config.action_controller.perform_caching = false - - config.cache_store = :null_store 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 - # Raise exceptions for disallowed deprecations. - config.active_support.disallowed_deprecation = :raise - - # Tell Active Support which deprecation messages to disallow. - config.active_support.disallowed_deprecation_warnings = [] - - # Suppress logger output for asset requests. - config.assets.quiet = true - # Raises error for missing translations. # config.i18n.raise_on_missing_translations = true diff --git a/examples/rails8/config/environments/production.rb b/examples/rails8/config/environments/production.rb new file mode 100644 index 000000000..16d20d19a --- /dev/null +++ b/examples/rails8/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 = false + + # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. + config.force_ssl = false + + # 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/examples/rails7/config/environments/test.rb b/examples/rails8/config/environments/test.rb similarity index 75% rename from examples/rails7/config/environments/test.rb rename to examples/rails8/config/environments/test.rb index 999db7091..14bc29e06 100644 --- a/examples/rails7/config/environments/test.rb +++ b/examples/rails8/config/environments/test.rb @@ -1,5 +1,3 @@ -require "active_support/core_ext/integer/time" - # 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 @@ -17,12 +15,11 @@ # 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=#{1.hour.to_i}" } + # 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 and disable caching. + # Show full error reports. config.consider_all_requests_local = true - config.action_controller.perform_caching = false config.cache_store = :null_store # Render exception templates for rescuable exceptions and raise for other exceptions. @@ -34,12 +31,6 @@ # Print deprecation notices to the stderr. config.active_support.deprecation = :stderr - # Raise exceptions for disallowed deprecations. - config.active_support.disallowed_deprecation = :raise - - # Tell Active Support which deprecation messages to disallow. - config.active_support.disallowed_deprecation_warnings = [] - # Raises error for missing translations. # config.i18n.raise_on_missing_translations = true diff --git a/examples/rails7/config/initializers/assets.rb b/examples/rails8/config/initializers/assets.rb similarity index 100% rename from examples/rails7/config/initializers/assets.rb rename to examples/rails8/config/initializers/assets.rb diff --git a/examples/rails7/config/initializers/content_security_policy.rb b/examples/rails8/config/initializers/content_security_policy.rb similarity index 80% rename from examples/rails7/config/initializers/content_security_policy.rb rename to examples/rails8/config/initializers/content_security_policy.rb index b3076b38f..d51d71397 100644 --- a/examples/rails7/config/initializers/content_security_policy.rb +++ b/examples/rails8/config/initializers/content_security_policy.rb @@ -20,6 +20,10 @@ # 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/examples/rails7/config/initializers/filter_parameter_logging.rb b/examples/rails8/config/initializers/filter_parameter_logging.rb similarity index 93% rename from examples/rails7/config/initializers/filter_parameter_logging.rb rename to examples/rails8/config/initializers/filter_parameter_logging.rb index c010b83dd..c0b717f7e 100644 --- a/examples/rails7/config/initializers/filter_parameter_logging.rb +++ b/examples/rails8/config/initializers/filter_parameter_logging.rb @@ -4,5 +4,5 @@ # 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 + :passw, :email, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn, :cvv, :cvc ] diff --git a/examples/rails7/config/initializers/inflections.rb b/examples/rails8/config/initializers/inflections.rb similarity index 100% rename from examples/rails7/config/initializers/inflections.rb rename to examples/rails8/config/initializers/inflections.rb diff --git a/examples/rails7/config/locales/en.yml b/examples/rails8/config/locales/en.yml similarity index 100% rename from examples/rails7/config/locales/en.yml rename to examples/rails8/config/locales/en.yml diff --git a/examples/rails7/config/routes.rb b/examples/rails8/config/routes.rb similarity index 67% rename from examples/rails7/config/routes.rb rename to examples/rails8/config/routes.rb index 277c82db6..0d8e1662f 100644 --- a/examples/rails7/config/routes.rb +++ b/examples/rails8/config/routes.rb @@ -5,8 +5,11 @@ # 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 - match ':controller(/:action(/:id))(.:format)', via: :all + # 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 + match ':controller(/:action(/:id))(.:format)', via: :all # Defines the root path route ("/") # root "posts#index" diff --git a/examples/rails7/config/warble.rb b/examples/rails8/config/warble.rb similarity index 99% rename from examples/rails7/config/warble.rb rename to examples/rails8/config/warble.rb index 0602f2229..22246965f 100644 --- a/examples/rails7/config/warble.rb +++ b/examples/rails8/config/warble.rb @@ -154,7 +154,7 @@ # config.pathmaps.public_html = ["%{public/,}p"] # Value of RAILS_ENV for the webapp -- default as shown below - # config.webxml.rails.env = ENV['RAILS_ENV'] || 'production' + config.webxml.rails.env = 'development' # Public ROOT mapping, by default assets are copied into .war ROOT directory. # config.public.root = '' diff --git a/examples/rails7/public/404.html b/examples/rails8/public/404.html similarity index 100% rename from examples/rails7/public/404.html rename to examples/rails8/public/404.html diff --git a/examples/rails7/public/406-unsupported-browser.html b/examples/rails8/public/406-unsupported-browser.html similarity index 100% rename from examples/rails7/public/406-unsupported-browser.html rename to examples/rails8/public/406-unsupported-browser.html diff --git a/examples/rails7/public/422.html b/examples/rails8/public/422.html similarity index 100% rename from examples/rails7/public/422.html rename to examples/rails8/public/422.html diff --git a/examples/rails7/public/500.html b/examples/rails8/public/500.html similarity index 100% rename from examples/rails7/public/500.html rename to examples/rails8/public/500.html diff --git a/examples/rails7/public/icon.png b/examples/rails8/public/icon.png similarity index 100% rename from examples/rails7/public/icon.png rename to examples/rails8/public/icon.png diff --git a/examples/rails7/public/icon.svg b/examples/rails8/public/icon.svg similarity index 100% rename from examples/rails7/public/icon.svg rename to examples/rails8/public/icon.svg diff --git a/examples/rails7/public/jsp/include.jsp b/examples/rails8/public/jsp/include.jsp similarity index 100% rename from examples/rails7/public/jsp/include.jsp rename to examples/rails8/public/jsp/include.jsp diff --git a/examples/rails7/public/jsp/index.jsp b/examples/rails8/public/jsp/index.jsp similarity index 100% rename from examples/rails7/public/jsp/index.jsp rename to examples/rails8/public/jsp/index.jsp diff --git a/examples/rails7/public/robots.txt b/examples/rails8/public/robots.txt similarity index 100% rename from examples/rails7/public/robots.txt rename to examples/rails8/public/robots.txt 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/lib/stream.rb b/examples/sinatra/lib/stream.rb index a4dac6d30..ec84a8e0e 100644 --- a/examples/sinatra/lib/stream.rb +++ b/examples/sinatra/lib/stream.rb @@ -27,3 +27,22 @@ def each get '/servertime' do ServerTime.new(response) end + +# Sanity check for JRuby-Rack's Rack 3 "streaming body" support: this route +# returns a body that responds to #call (and NOT #each), i.e. a Rack 3.x +# streaming body - unlike /servertime above which is an #each (enumerable) body. +# JRuby-Rack hands the body a `stream` wrapping the servlet output; each write +# is flushed, so chunks arrive one-per-second. Watch it with: +# curl -N http://localhost:8080//stream_call +get '/stream_call' do + streaming_body = lambda do |stream| + 5.times do |i| + stream.write "chunk #{i} @ #{Time.now.strftime('%H:%M:%S')}\n" + sleep 1 + end + ensure + stream.close + end + # return a raw Rack response tuple so Sinatra passes the #call body through + halt 200, { 'content-type' => 'text/plain', 'x-accel-buffering' => 'no' }, streaming_body +end 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 @@
This line of text came from ERB.

-
And this is content from a JSP: <%= request.render "/jsp/include.jsp", {"message" => "I'm a message that came from Sinatra!"} %>
+
And this is content from a JSP:
<%= request.render "/jsp/include.jsp", {"message" => "I'm a message that came from Sinatra!"} %>
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/java/org/jruby/rack/ext/Response.java b/src/main/java/org/jruby/rack/ext/Response.java index 63f37fd55..3e68136a8 100644 --- a/src/main/java/org/jruby/rack/ext/Response.java +++ b/src/main/java/org/jruby/rack/ext/Response.java @@ -242,7 +242,7 @@ public IRubyObject initialize(final ThreadContext context, final IRubyObject arg this.body = arg.callMethod(context, "[]", context.runtime.newFixnum(2)); } // HACK: deal with objects that don't comply with Rack specification - if ( ! this.body.respondsTo("each_line") && ! this.body.respondsTo("each") ) { + if ( ! this.body.respondsTo("each_line") && ! this.body.respondsTo("each") && ! this.body.respondsTo("call") ) { this.body = this.body.asString(); // previously @body = [ @body.to_s ] } return this; @@ -355,8 +355,7 @@ protected void writeStatus(final RackResponseEnvironment response) { } @JRubyMethod(name = "write_headers") - public IRubyObject write_headers(final ThreadContext context, final IRubyObject response) - throws IOException { + public IRubyObject write_headers(final ThreadContext context, final IRubyObject response) { writeHeaders(response.toJava(RackResponseEnvironment.class)); return context.nil; } @@ -366,27 +365,35 @@ public IRubyObject write_headers(final ThreadContext context, final IRubyObject protected void writeHeaders(final RackResponseEnvironment response) { this.headers.visitAll(currentContext(), new RubyHash.Visitor() { // headers.each { |key, val| } @Override - public void visit(final IRubyObject key, final IRubyObject val) { + public void visit(final IRubyObject key, IRubyObject val) { final String name = key.toString(); // SPEC: special headers starting "rack." are for communicating // with the server and must not be sent back to the client if ( name.startsWith("rack.") ) return; - if ( name.equalsIgnoreCase("Content-Type") ) { - response.setContentType( val.asJavaString() ); return; + // SPEC (Rack 3.x): a header value might be an Array of Strings, + // unwrap single values - multi values do plain addHeader below + if ( val instanceof RubyArray valArr && valArr.size() == 1 ) { + val = valArr.eltOk(0); } - if ( name.equalsIgnoreCase("Content-Length") ) { - if ( isChunked() ) return; - final long length = val.convertToInteger("to_i").asLong(currentContext()); - if ( length < Integer.MAX_VALUE ) { - response.setContentLength( (int) length ); return; - } // else will do addHeader - } + if ( ! (val instanceof RubyArray) ) { + if ( name.equalsIgnoreCase("Content-Type") ) { + response.setContentType( val.asJavaString() ); return; + } - if ( name.equalsIgnoreCase("Transfer-Encoding") ) { - if ( skipEncodingHeader(val) ) return; + if ( name.equalsIgnoreCase("Content-Length") ) { + if ( isChunked() ) return; + final long length = val.convertToInteger("to_i").asLong(currentContext()); + if ( length < Integer.MAX_VALUE ) { + response.setContentLength( (int) length ); return; + } // else will do addHeader + } + + if ( name.equalsIgnoreCase("Transfer-Encoding") ) { + if ( skipEncodingHeader(val) ) return; + } } // NOTE: effectively the same as `v.split("\n").each` which is what @@ -404,7 +411,7 @@ public IRubyObject yield(ThreadContext context, IRubyObject[] args) { @Override public IRubyObject yield(ThreadContext context, IRubyObject value) { - value.callMethod(context, "chomp!", newLine); + value = value.callMethod(context, "chomp", newLine); response.addHeader(name, value.toString()); return value; } @@ -440,10 +447,9 @@ protected void writeBody(final RackResponseEnvironment response) throws IOExcept final ThreadContext context = currentContext(); Channel bodyChannel = null; IRubyObject body = this.body; try { - if ( body.respondsTo("call") && ! body.respondsTo("each") ) { - final IRubyObject outputStream = - JavaUtil.convertJavaToRuby(context.runtime, response.getOutputStream()); - this.body.callMethod(context, "call", outputStream); + if ( body.respondsTo("call") && ! body.respondsTo("each") ) { // Rack 3 streaming body + final IRubyObject outputStream = JavaUtil.convertJavaToRuby(context.runtime, response.getOutputStream()); + callMethod(context, "write_streaming_body", outputStream); return; } @@ -477,7 +483,6 @@ protected void writeBody(final RackResponseEnvironment response) throws IOExcept // NOTE: we no longer handle "to_inputstream" since in 1.7 "to_channel" covers those ... final OutputStream output = response.getOutputStream(); - IOException error = null; if ( doDechunk() ) { final IRubyObject output_stream = JavaUtil.convertJavaToRuby(context.runtime, output); callMethod(context, "write_body_dechunked", output_stream); @@ -487,20 +492,21 @@ protected void writeBody(final RackResponseEnvironment response) throws IOExcept try { invoke(context, body, method, new JavaInternalBlockBody(context.runtime, Signature.ONE_REQUIRED) { - @Override - public IRubyObject yield(ThreadContext context, IRubyObject[] args) { - return this.yield(context, args[0]); - } + @Override + public IRubyObject yield(ThreadContext context, IRubyObject[] args) { + return this.yield(context, args[0]); + } - @Override - public IRubyObject yield(ThreadContext context, IRubyObject line) { - try { - output.write( line.asString().getBytes() ); - if ( doFlush() ) output.flush(); + @Override + public IRubyObject yield(ThreadContext context, IRubyObject line) { + try { + output.write(line.asString().getBytes()); + if (doFlush()) output.flush(); + } catch (IOException e) { + throw new WrappedException(e); + } + return context.nil; } - catch (IOException e) { throw new WrappedException(e); } - return context.nil; - } }); } catch (WrappedException e) { throw e.getIOCause(); } @@ -573,7 +579,7 @@ public IRubyObject chunked_p(final ThreadContext context) { public boolean isChunked() { if ( chunked != null ) return chunked; if ( this.headers != null ) { - final IRubyObject value = getHeaderValue(TRANSFER_ENCODING, TRANSFER_ENCODING_LOWER); + final IRubyObject value = getHeaderValue(TRANSFER_ENCODING_LOWER, TRANSFER_ENCODING); if ( value instanceof RubyString rubyString) { return chunked = rubyString.getByteList().equal(CHUNKED); } @@ -582,13 +588,13 @@ public boolean isChunked() { } /** - * Rack does not mandate response header name casing - apps might use the - * conventional Capitalized-Names or (Rack 3.x style) lower-case names. + * Rack 3.x response header names are lower-case while Rack 2.x used + * Capitalized-Names, thus the (Rack 3) lower-case name is tried first. */ - private IRubyObject getHeaderValue(final ByteList canonicalName, final ByteList lowerCaseName) { - IRubyObject value = this.headers.callMethod("[]", RubyString.newString(getRuntime(), canonicalName)); + private IRubyObject getHeaderValue(final ByteList lowerCaseName, final ByteList canonicalName) { + IRubyObject value = this.headers.callMethod("[]", RubyString.newString(getRuntime(), lowerCaseName)); if ( value.isNil() ) { - value = this.headers.callMethod("[]", RubyString.newString(getRuntime(), lowerCaseName)); + value = this.headers.callMethod("[]", RubyString.newString(getRuntime(), canonicalName)); } return value; } @@ -615,7 +621,7 @@ protected boolean doFlush() { if ( isChunked() ) return true; if ( this.headers != null ) { // does not have a Content-Length header : - return getHeaderValue(CONTENT_LENGTH, CONTENT_LENGTH_LOWER).isNil(); + return getHeaderValue(CONTENT_LENGTH_LOWER, CONTENT_LENGTH).isNil(); } return false; } @@ -702,6 +708,7 @@ private void transferChannel(final ReadableByteChannel channel, final OutputStre private ThreadContext currentContext() { return getRuntime().getCurrentContext(); } + @SuppressWarnings("UnusedReturnValue") static IRubyObject invoke( final ThreadContext context, final IRubyObject self, final String method, final BlockBody body) { 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.rb b/src/main/ruby/jruby/rack/error_app.rb index ec19dd4ed..287548e37 100644 --- a/src/main/ruby/jruby/rack/error_app.rb +++ b/src/main/ruby/jruby/rack/error_app.rb @@ -4,11 +4,29 @@ #++ require 'jruby/rack' +require 'rack' module JRuby module Rack class ErrorApp + # Response header names matching the loaded Rack version's conventions: + # Rack 3.x requires lower-case header names, Rack 2.x used Capitalized + # names (e.g. its Rack::Cascade only recognizes 'X-Cascade'). + if ::Rack.release >= '3' + CONTENT_TYPE = 'content-type' + CONTENT_LENGTH = 'content-length' + LAST_MODIFIED = 'last-modified' + ALLOW = 'allow' + X_CASCADE = 'x-cascade' + else + CONTENT_TYPE = 'Content-Type' + CONTENT_LENGTH = 'Content-Length' + LAST_MODIFIED = 'Last-Modified' + ALLOW = 'Allow' + X_CASCADE = 'X-Cascade' + end + autoload :ShowStatus, 'jruby/rack/error_app/show_status' # @private @@ -41,7 +59,7 @@ def initialize(root = nil) def call(env) if env['REQUEST_METHOD'] == 'OPTIONS' - return [ 200, {'Allow' => ALLOW_METHODS, 'Content-Length' => '0'}, [] ] + return [ 200, { ALLOW => ALLOW_METHODS, CONTENT_LENGTH => '0' }, [] ] end code = response_code(env) @@ -79,17 +97,17 @@ def serve(code, path, env) last_modified = File.mtime(path).httpdate return [ 304, {}, [] ] if env['HTTP_IF_MODIFIED_SINCE'] == last_modified - headers = { 'Last-Modified' => last_modified } + headers = { LAST_MODIFIED => last_modified } DEFAULT_HEADERS.each { |field, content| headers[field] = content } ext = File.extname(path) size = File.size?(path) mime = ::Rack::Mime.mime_type(ext, DEFAULT_MIME) - headers['Content-Type'] = mime + headers[CONTENT_TYPE] = mime body = env['REQUEST_METHOD'] == 'HEAD' ? [] : FileBody.new(path, size) response = [ code, headers, body ] - response[1]['Content-Length'] = size.to_s if size + response[1][CONTENT_LENGTH] = size.to_s if size response end @@ -108,9 +126,9 @@ def map_error_code(exc) def respond(status = nil, body = nil, headers = DEFAULT_HEADERS.dup) status ||= DEFAULT_RESPONSE_CODE body += "\n" if body - headers['Content-Type'] = "text/plain" unless headers.key?('Content-Type') - headers['Content-Length'] = body.size.to_s if ! headers.key?('Content-Length') && body - headers['X-Cascade'] = "pass" unless headers.key?('X-Cascade') + headers[CONTENT_TYPE] = "text/plain" unless headers.key?(CONTENT_TYPE) + headers[CONTENT_LENGTH] = body.size.to_s if ! headers.key?(CONTENT_LENGTH) && body + headers[X_CASCADE] = "pass" unless headers.key?(X_CASCADE) [ status, headers, body ? [ body ] : [] ] end 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..c2fb6afbd 100644 --- a/src/main/ruby/jruby/rack/error_app/show_status.rb +++ b/src/main/ruby/jruby/rack/error_app/show_status.rb @@ -15,8 +15,8 @@ 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 + # a custom error app might use either header name casing convention : + empty = (headers['Content-Length'] || headers['content-length']).to_i <= 0 detail = env['rack.showstatus.detail'] # client or server error, or explicit message @@ -30,7 +30,7 @@ def call(env) body = @template.result(binding) size = body.bytesize - [status, headers.merge('Content-Type' => "text/html", 'Content-Length' => size.to_s), [body]] + [status, headers.merge(CONTENT_TYPE => "text/html", CONTENT_LENGTH => size.to_s), [body]] else [status, headers, body] end diff --git a/src/main/ruby/jruby/rack/response.rb b/src/main/ruby/jruby/rack/response.rb index 9ae8cd9db..a6a647af5 100644 --- a/src/main/ruby/jruby/rack/response.rb +++ b/src/main/ruby/jruby/rack/response.rb @@ -23,6 +23,19 @@ class Response private + # Writes a Rack 3 "streaming" response body - one that responds to +call+ + # (and not +each+) - by handing it a +stream+ that wraps the (servlet) + # response output. The body's +call+ is invoked once; the stream is + # always closed afterwards. + # @see https://github.com/rack/rack/blob/main/SPEC.rdoc ("Streaming Body") + # @note invoked from `org.jruby.rack.ext.Response#writeBody` + def write_streaming_body(output_stream) + stream = Stream.new(output_stream) + body.call(stream) + ensure + stream.close if stream + end + def write_body_dechunked(output_stream) # NOTE: due Rails 3.2 stream-ed rendering http://git.io/ooCOtA#L223 # Only required if the patch at jruby/rack/chunked.rb is not applied ... @@ -48,6 +61,71 @@ def write_body_dechunked(output_stream) end end + # The +stream+ handed to a streaming (`#call`) response body. Wraps the + # (servlet) response output stream to provide the +IO+-like interface the + # Rack SPEC requires of a streaming body's stream argument: +read+, + # +write+, +<<+, +flush+, +close+, +close_read+, +close_write+, +closed?+. + # + # It is write-only - a servlet response output stream cannot be read from + # (full bi-directional hijack is not possible over the servlet API) - so + # +read+/+close_read+ are no-ops. This still covers the common case of + # one-directional streaming (SSE, long-poll, progressive rendering). + # + # @private only handed to a streaming body by #write_streaming_body + class Stream + + def initialize(output_stream) + @output = output_stream + @closed = false + end + + def closed? + @closed + end + + # Writes the given data, flushing so the client receives it promptly + # (the whole point of a streaming body). Returns the number of bytes. + def write(data) + raise IOError, 'closed stream' if closed? + string = data.is_a?(String) ? data : data.to_s + @output.write(string.to_java_bytes) + @output.flush + string.bytesize + end + + def <<(data) + write(data) + self + end + + def flush + @output.flush unless closed? + self + end + + # A response stream is write-only; there is nothing to read. + def read(*) + nil + end + + def close_read + nil + end + + def close + return if closed? + @closed = true + begin + @output.close + rescue java.io.IOException, java.lang.IllegalStateException + # response already committed / client gone - nothing we can do + end + nil + end + alias_method :close_write, :close + end + private_constant :Stream + end end end diff --git a/src/main/ruby/rack/handler/servlet/default_env.rb b/src/main/ruby/rack/handler/servlet/default_env.rb index 40edc3fc3..af3c0ebd5 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 @@ -172,10 +179,27 @@ def load_headers for name in header_names next if name =~ @@content_header_names key = "HTTP_#{name.upcase.gsub(/-/, '_')}".freeze - @env[key] = @servlet_env.getHeader(name) unless @env.key?(key) + @env[key] = header_value(name) unless @env.key?(key) end end + # Joins all values of a (repeated) request header into the single value + # Rack expects - getHeader would only return the first one. Cookie + # headers are re-combined using '; ' as of RFC 6265 / RFC 7540. + def header_value(name) + headers = @servlet_env.getHeaders(name) + # might return null if the container does not allow header access : + return @servlet_env.getHeader(name) if headers.nil? + value = nil + separator = name.to_s.casecmp('Cookie') == 0 ? '; ' : ', ' + while headers.hasMoreElements + header = headers.nextElement + value = value.nil? ? header : "#{value}#{separator}#{header}" + end + value + end + private :header_value + def load_env_key(env, key) return unless @servlet_env if key[0, 5] == 'HTTP_' @@ -192,7 +216,7 @@ def load_header(env, key) name = key.sub('HTTP_', ''). split('_').each { |w| w.downcase!; w.capitalize! }.join('-') return if name =~ @@content_header_names - if header = @servlet_env.getHeader(name) + if header = header_value(name) env[key] = header # null if it does not have a header of that name end end @@ -216,6 +240,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 +255,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..c253a0691 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 @@ -47,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 @@ -57,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 @@ -82,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 @@ -96,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/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/error_app_spec.rb b/src/spec/ruby/jruby/rack/error_app_spec.rb index e82c1a35f..1fb156e68 100644 --- a/src/spec/ruby/jruby/rack/error_app_spec.rb +++ b/src/spec/ruby/jruby/rack/error_app_spec.rb @@ -55,9 +55,9 @@ response = error_app.call(@env) expect(response[0]).to eql 503 - expect(response[1]).to include 'Last-Modified' - expect(response[1]).to include 'Content-Length' - expect(response[1]['Content-Type']).to eql 'text/html' + expect(response[1]).to include header('Last-Modified') + expect(response[1]).to include header('Content-Length') + expect(response[1][header('Content-Type')]).to eql 'text/html' expect(body = response[2]).to be_a JRuby::Rack::ErrorApp::FileBody content = ''; body.each { |chunk| content << chunk } expect(content).to eql '-503-' @@ -74,21 +74,31 @@ response = error_app.call(@env) expect(response[0]).to eql 500 # 503 - expect(response[1]).to include 'Content-Length' - expect(response[1]['Content-Type']).to eql 'text/html' + expect(response[1]).to include header('Content-Length') + expect(response[1][header('Content-Type')]).to eql 'text/html' expect(body = response[2]).to be_a JRuby::Rack::ErrorApp::FileBody content = ''; body.each { |chunk| content << chunk } expect(content).to eql _500_html end end + it "uses header casing matching the Rack version" do + init_exception + response = error_app.call(@env) + if Rack.release >= '3' + expect(response[1].keys).to include 'content-type', 'x-cascade' + else + expect(response[1].keys).to include 'Content-Type', 'X-Cascade' + end + end + it "returns a fresh headers hash for each response" do init_exception response1 = error_app.call(@env) - response1[1]['X-Polluted'] = 'leaked' + response1[1]['x-polluted'] = 'leaked' response2 = error_app.call(@env) - expect(response2[1]).to_not include 'X-Polluted' + expect(response2[1]).to_not include 'x-polluted' expect(JRuby::Rack::ErrorApp::DEFAULT_HEADERS).to be_empty end @@ -131,6 +141,19 @@ def message body = response[2][0] expect(body).to include 'Internal Server Error' expect(body).to match /
\n\s{4}

something went wrong<\/p>\n\s{2}<\/div>/m + + expect(response[1][header('Content-Type')]).to eql 'text/html' + expect(response[1][header('Content-Length')]).to eql body.bytesize.to_s + end + + it "detects a non-empty response regardless of content-length header casing" do + [ 'Content-Length', 'content-length' ].each do |content_length| + app = lambda { |env| [ 500, { content_length => '5' }, [ '12345' ] ] } + show_status = JRuby::Rack::ErrorApp::ShowStatus.new app + + response = show_status.call(@env) + expect(response[2]).to eql [ '12345' ] # body not replaced by template + end end it "does not render detail info when 'rack.showstatus.detail' set to false" do @@ -173,6 +196,10 @@ def error_app.map_error_code(exc) private + def header(name) + Rack.release >= '3' ? name.downcase : name + end + def init_exception(cause = nil) exception = org.jruby.rack.RackInitializationException.new("something went wrong", cause) @env[org.jruby.rack.RackEnvironment::EXCEPTION] = exception 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/jruby/rack/response_spec.rb b/src/spec/ruby/jruby/rack/response_spec.rb index eb44b8731..dc3b8a079 100644 --- a/src/spec/ruby/jruby/rack/response_spec.rb +++ b/src/spec/ruby/jruby/rack/response_spec.rb @@ -86,6 +86,13 @@ class << value response.write_headers(response_environment) end + it "writes frozen header values containing newlines without raising" do + response.to_java.getHeaders.update({ "Set-Cookie" => "cookie1\ncookie2".freeze }) + expect(servlet_response).to receive(:addHeader).with("Set-Cookie", "cookie1") + expect(servlet_response).to receive(:addHeader).with("Set-Cookie", "cookie2") + response.write_headers(response_environment) + end + it "adds an int header when values is a fixnum" do update_response_headers "Expires" => 0 expect(response_environment).to receive(:addIntHeader).with("Expires", 0) @@ -137,6 +144,22 @@ class << value expect(response.chunked?).to be true end + it "handles (single value) Array header values for special-cased headers (Rack 3.x)" do + headers = { "content-type" => [ "text/html" ], "content-length" => [ "5" ] } + response = JRuby::Rack::Response.new [200, headers, ['hello']] + expect(servlet_response).to receive(:setContentType).with("text/html") + expect(servlet_response).to receive(:setContentLength).with(5) + response.write_headers(response_environment) + end + + it "adds multi value Array special-cased headers without raising (Rack 3.x)" do + headers = { "content-type" => [ "text/html", "text/plain" ] } + response = JRuby::Rack::Response.new [200, headers, ['hello']] + expect(servlet_response).to receive(:addHeader).with("content-type", "text/html") + expect(servlet_response).to receive(:addHeader).with("content-type", "text/plain") + response.write_headers(response_environment) + end + it "detects a chunked response with a lower-case transfer-encoding header" do headers = { "transfer-encoding" => "chunked" } response = JRuby::Rack::Response.new [200, headers, ['body']] @@ -422,6 +445,53 @@ def body.each_line response.write_body(response_environment) end + it "writes a streaming (#call) body via a stream wrapping the output" do + body = lambda do |out| + out.write "hello " + out << "there" + out.close + end + response = JRuby::Rack::Response.new [200, {}, body] + response.write_body(response_environment) + expect(stream.to_s).to eq "hello there" + end + + it "does not stringify a streaming (#call) body" do + body = lambda { |out| out.close } + response = JRuby::Rack::Response.new [200, {}, body] + expect(response.body).to be body # not coerced to a String + end + + it "hands the streaming body a Rack SPEC compliant stream and closes it" do + seen = nil + body = lambda { |out| seen = out } + response = JRuby::Rack::Response.new [200, {}, body] + response.write_body(response_environment) + + %i[read write << flush close close_read close_write closed?].each do |method| + expect(seen).to respond_to(method) + end + expect(seen.closed?).to be true # stream always closed after the body returns + end + + it "flushes the output after each streamed write, then closes it" do + body = lambda do |out| + out.write "a" + out << "b" + end + response = JRuby::Rack::Response.new [200, {}, body] + + # streaming means each write reaches the client promptly (write then flush) + # and the underlying output is closed once the body returns + expect(stream).to receive(:write).ordered + expect(stream).to receive(:flush).ordered + expect(stream).to receive(:write).ordered + expect(stream).to receive(:flush).ordered + expect(stream).to receive(:close).ordered + + response.write_body(response_environment) + end + private def wrap_file_body(path) 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..cb578cd3a 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" @@ -404,6 +410,17 @@ def getAttributeNames expect { env.fetch('attr4') }.to raise_error # KeyError end + it "joins the values of repeated request headers" do + @servlet_request.addHeader "X-Forwarded-For", "10.0.0.1" + @servlet_request.addHeader "X-Forwarded-For", "10.0.0.2" + @servlet_request.addHeader "Cookie", "foo=1" + @servlet_request.addHeader "Cookie", "bar=2" + + env = servlet.create_env(@servlet_env) + expect(env['HTTP_X_FORWARDED_FOR']).to eq "10.0.0.1, 10.0.0.2" + expect(env['HTTP_COOKIE']).to eq "foo=1; bar=2" # RFC 6265 cookie separator + end + end shared_examples "(eager)rack-env" do @@ -455,6 +472,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 +480,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 +516,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 +650,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 +795,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 +803,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') @@ -921,6 +956,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' @@ -928,6 +972,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