From 2474be612c090609810ca100b077885e4a585497 Mon Sep 17 00:00:00 2001 From: Yuki Nishijima Date: Sat, 23 Sep 2023 14:51:36 +0900 Subject: [PATCH 1/7] WIP --- .gitignore | 1 + lib/rambulance/exceptions_app.rb | 13 +++++++++++++ lib/rambulance/railtie.rb | 13 +++++++++++++ lib/tasks/rambulance.rake | 12 ++++++++++++ test/exceptions_app_test.rb | 17 ++++++++++++++++- test/fake_app/public/.keep | 0 6 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 lib/tasks/rambulance.rake create mode 100644 test/fake_app/public/.keep diff --git a/.gitignore b/.gitignore index b061478..02ecade 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ tmp *.a mkmf.log *.log +test/fake_app/public/*.html diff --git a/lib/rambulance/exceptions_app.rb b/lib/rambulance/exceptions_app.rb index ff5066b..22dea24 100644 --- a/lib/rambulance/exceptions_app.rb +++ b/lib/rambulance/exceptions_app.rb @@ -45,6 +45,19 @@ def self.local_prefixes [Rambulance.view_path] end + def self.precompile!(env: {}, assigns: {}) + ERROR_HTTP_STATUSES.each do |http_status, status_in_words| + begin + html = renderer.new(env).render(status_in_words, assigns: assigns) + path = Rails.public_path.join("#{http_status}.html").to_s + + File.write(path, html) + rescue ActionView::MissingTemplate + Rails.logger.info "Template for #{http_status}(#{status_in_words}) does not exist. Skiping..." + end + end + end + ERROR_HTTP_STATUSES.values.each do |status_in_words| eval <<-ACTION, nil, __FILE__, __LINE__ + 1 def #{status_in_words} diff --git a/lib/rambulance/railtie.rb b/lib/rambulance/railtie.rb index 783fe69..20b6740 100644 --- a/lib/rambulance/railtie.rb +++ b/lib/rambulance/railtie.rb @@ -1,5 +1,8 @@ module Rambulance class Railtie < Rails::Railtie + config.rambulance = ActiveSupport::OrderedOptions.new + config.rambulance.static_error_pages = false + initializer 'rambulance', after: :prepend_helpers_path do |app| ActiveSupport.on_load(:action_controller) do require "rambulance/exceptions_app" @@ -33,5 +36,15 @@ class Railtie < Rails::Railtie end if Rails.env.development? end end + + rake_tasks do + require 'rambulance/exceptions_app' + + if config.rambulance.static_error_pages + Rake::Task["assets:precompile"].enhance do + Rake::Task["rambulance:precompile"].invoke + end + end + end end end diff --git a/lib/tasks/rambulance.rake b/lib/tasks/rambulance.rake new file mode 100644 index 0000000..c0a96d5 --- /dev/null +++ b/lib/tasks/rambulance.rake @@ -0,0 +1,12 @@ +namespace :rambulance do + desc "Precompiles static HTML files for each error status" + task precompile: :environment do + exceptions_app = begin + ::ExceptionsApp + rescue NameError + Rambulance::ExceptionsApp + end + + exceptions_app.precompile! + end +end diff --git a/test/exceptions_app_test.rb b/test/exceptions_app_test.rb index b9a85fc..1174f86 100644 --- a/test/exceptions_app_test.rb +++ b/test/exceptions_app_test.rb @@ -2,8 +2,23 @@ class ExeptionsAppTest < ActionDispatch::IntegrationTest test 'returns 404 for unknown format for pages that do not exist' do - get '/does_not_exist', headers: { 'Accept' => '*/*' } + get '/does_not_exist', headers: { 'Accept' => '*/*' } assert_equal 404, response.status end + + test '#precompile! generates static HTML files for each error status' do + Dir[Rails.public_path.join("*.html")].each do |file| + File.delete(file) + end + + Rambulance::ExceptionsApp.precompile! + + assert File.exist?(Rails.public_path.join("400.html")) + assert File.exist?(Rails.public_path.join("403.html")) + assert File.exist?(Rails.public_path.join("404.html")) + assert File.exist?(Rails.public_path.join("406.html")) + assert File.exist?(Rails.public_path.join("500.html")) + assert_not File.exist?(Rails.public_path.join("401.html")) + end end diff --git a/test/fake_app/public/.keep b/test/fake_app/public/.keep new file mode 100644 index 0000000..e69de29 From 26030f519277764e144bbd8222b94c3bd9f8f70e Mon Sep 17 00:00:00 2001 From: Yuki Nishijima <386234+yuki24@users.noreply.github.com> Date: Sat, 23 Sep 2023 15:46:07 +0900 Subject: [PATCH 2/7] Do not test .precompile! method for Rails 4.2 --- test/exceptions_app_test.rb | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/test/exceptions_app_test.rb b/test/exceptions_app_test.rb index 1174f86..eb54573 100644 --- a/test/exceptions_app_test.rb +++ b/test/exceptions_app_test.rb @@ -7,18 +7,20 @@ class ExeptionsAppTest < ActionDispatch::IntegrationTest assert_equal 404, response.status end - test '#precompile! generates static HTML files for each error status' do - Dir[Rails.public_path.join("*.html")].each do |file| - File.delete(file) + if Rails.version > '4.2.0' + test '#precompile! generates static HTML files for each error status' do + Dir[Rails.public_path.join("*.html")].each do |file| + File.delete(file) + end + + Rambulance::ExceptionsApp.precompile! + + assert File.exist?(Rails.public_path.join("400.html")) + assert File.exist?(Rails.public_path.join("403.html")) + assert File.exist?(Rails.public_path.join("404.html")) + assert File.exist?(Rails.public_path.join("406.html")) + assert File.exist?(Rails.public_path.join("500.html")) + assert_not File.exist?(Rails.public_path.join("401.html")) end - - Rambulance::ExceptionsApp.precompile! - - assert File.exist?(Rails.public_path.join("400.html")) - assert File.exist?(Rails.public_path.join("403.html")) - assert File.exist?(Rails.public_path.join("404.html")) - assert File.exist?(Rails.public_path.join("406.html")) - assert File.exist?(Rails.public_path.join("500.html")) - assert_not File.exist?(Rails.public_path.join("401.html")) end end From 4314b04693f624768c8aabaec6fd03d37bb926d6 Mon Sep 17 00:00:00 2001 From: Yuki Nishijima <386234+yuki24@users.noreply.github.com> Date: Sat, 23 Sep 2023 15:48:08 +0900 Subject: [PATCH 3/7] Update exceptions_app_test.rb --- test/exceptions_app_test.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/exceptions_app_test.rb b/test/exceptions_app_test.rb index eb54573..0d376eb 100644 --- a/test/exceptions_app_test.rb +++ b/test/exceptions_app_test.rb @@ -7,7 +7,7 @@ class ExeptionsAppTest < ActionDispatch::IntegrationTest assert_equal 404, response.status end - if Rails.version > '4.2.0' + if Rails.version >= '5.0.0' test '#precompile! generates static HTML files for each error status' do Dir[Rails.public_path.join("*.html")].each do |file| File.delete(file) From 90f696dd1e1decb189ea4ac50f42b52a0db21867 Mon Sep 17 00:00:00 2001 From: Yuki Nishijima <386234+yuki24@users.noreply.github.com> Date: Sat, 23 Sep 2023 16:10:13 +0900 Subject: [PATCH 4/7] Update README.md --- README.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/README.md b/README.md index 0894ae3..24b0c71 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,35 @@ It will generate your own custom exceptions app. You can use whatever techniques **Heavily customizing the exceptions app is strongly discouraged as there would be no guard against bugs that occur in the exceptions app.** +## Static Error Page Precompilation + +Generating error pages dynamically can be risky at times, but it isn't always necessary, especially when the error page doesn't rely on dynamic data such as authentication information to render the entire page. What's more crucial when constructing an error page is ensuring the use of the same assets as the main application and static pages. In such cases, error pages could be generated during the asset pre-completion phase, rather than at runtime. + +As of version 3.1.0, Rambulance provides a way to precompile error pages at the time of asset precomplication. + +### How to Use the Static Error Page Precomplation Feature + +TBD + +```ruby +# config/initializers/rambulance.rb +config.static_error_pages = true +``` + +### FAQ & Troubleshooting + +#### What are the main differences between the dynamic mode and static mode? + +TBD + +#### I'm using `devise` and can't precompile error pages + +If you are using Devise, then an error `Devise could not find the 'Warden::Proxy' instance on your request environment` may occur, as it monkey-patches the `ActionController::Base` class. The code below illustrates how the proxy object could be fulfilled manually: + +```ruby +TBD +``` + ## Testing Rambulance ships with a test helper that allows you to test an error page generated by Rails. All you have to do is to `include Rambulance::TestHelper` and you will be able to use the `with_exceptions_app` DSDL: From d1bbdeeebead810a819c53b7e9389e52ed79d1d9 Mon Sep 17 00:00:00 2001 From: Yuki Nishijima <386234+yuki24@users.noreply.github.com> Date: Sat, 23 Sep 2023 17:03:31 +0900 Subject: [PATCH 5/7] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 24b0c71..4561985 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ TBD ```ruby # config/initializers/rambulance.rb -config.static_error_pages = true +config.static_error_pages = Rails.env.production? ``` ### FAQ & Troubleshooting From fa318b637ae8c9c95a835c8cfd989708daad07e8 Mon Sep 17 00:00:00 2001 From: Yuki Nishijima Date: Sat, 23 Sep 2023 21:18:01 +0900 Subject: [PATCH 6/7] Add tests for the rake task --- Rakefile | 7 ++- lib/rambulance/railtie.rb | 55 +++++++++++++----------- test/fake_app/rails_app.rb | 6 +++ test/requests/static_error_pages_test.rb | 27 ++++++++++++ 4 files changed, 69 insertions(+), 26 deletions(-) create mode 100644 test/requests/static_error_pages_test.rb diff --git a/Rakefile b/Rakefile index 170ec01..c9ac0c7 100644 --- a/Rakefile +++ b/Rakefile @@ -14,7 +14,12 @@ namespace :test do task :custom do sh "rake test:default CUSTOM_EXCEPTIONS_APP=1" end + + desc "Run tests for the static page mode" + task :static_pages do + sh "TEST=test/requests/static_error_pages_test.rb STATIC_ERROR_PAGES=1 rake test:default" + end end -task(:test).enhance %w(test:default test:custom) +task(:test).enhance %w(test:default test:custom test:static_pages) task default: :test diff --git a/lib/rambulance/railtie.rb b/lib/rambulance/railtie.rb index 20b6740..b5d805e 100644 --- a/lib/rambulance/railtie.rb +++ b/lib/rambulance/railtie.rb @@ -8,39 +8,44 @@ class Railtie < Rails::Railtie require "rambulance/exceptions_app" end - app.config.exceptions_app = - if app.config.respond_to?(:autoloader) && app.config.autoloader == :classic - ->(env) { - begin - ActiveSupport::Dependencies.load_missing_constant(Object, :ExceptionsApp) - ::ExceptionsApp.call(env) - rescue NameError - require "rambulance/exceptions_app" if !defined?(::Rambulance::ExceptionsApp) - ::Rambulance::ExceptionsApp.call(env) - end - } - else - ->(env) { - begin - ::ExceptionsApp.call(env) - rescue NameError - require "rambulance/exceptions_app" if !defined?(::Rambulance::ExceptionsApp) - ::Rambulance::ExceptionsApp.call(env) - end - } - end + exceptions_app = if app.config.respond_to?(:autoloader) && app.config.autoloader == :classic + ->(env) { + begin + ActiveSupport::Dependencies.load_missing_constant(Object, :ExceptionsApp) + ::ExceptionsApp.call(env) + rescue NameError + require "rambulance/exceptions_app" if !defined?(::Rambulance::ExceptionsApp) + ::Rambulance::ExceptionsApp.call(env) + end + } + else + ->(env) { + begin + ::ExceptionsApp.call(env) + rescue NameError + require "rambulance/exceptions_app" if !defined?(::Rambulance::ExceptionsApp) + ::Rambulance::ExceptionsApp.call(env) + end + } + end + + if !app.config.rambulance.static_error_pages + app.config.exceptions_app = exceptions_app + end ActiveSupport.on_load(:after_initialize) do - Rails.application.routes.append do - mount app.config.exceptions_app, at: '/rambulance' - end if Rails.env.development? + if Rails.env.development? + Rails.application.routes.append do + mount exceptions_app, at: '/rambulance' + end + end end end rake_tasks do require 'rambulance/exceptions_app' - if config.rambulance.static_error_pages + if config.rambulance.static_error_pages && Rake::Task.task_defined?("assets:precompile") Rake::Task["assets:precompile"].enhance do Rake::Task["rambulance:precompile"].invoke end diff --git a/test/fake_app/rails_app.rb b/test/fake_app/rails_app.rb index b1be908..16676dc 100644 --- a/test/fake_app/rails_app.rb +++ b/test/fake_app/rails_app.rb @@ -1,4 +1,8 @@ require 'jbuilder' +require 'rake' + +# This task needs to be defined before the Rails application is loaded so Rambulance can override it. +Rake::Task.define_task('assets:precompile') # config class TestApp < Rails::Application @@ -10,12 +14,14 @@ class TestApp < Rails::Application config.root = File.dirname(__FILE__) config.autoload_paths += ["#{config.root}/lib"] if ENV["CUSTOM_EXCEPTIONS_APP"] config.hosts = "www.example.com" + config.rambulance.static_error_pages = !!ENV["STATIC_ERROR_PAGES"] if Rails::VERSION::STRING >= "5.2" config.action_controller.default_protect_from_forgery = true end end Rails.backtrace_cleaner.remove_silencers! +Rails.application.load_tasks if ENV["STATIC_ERROR_PAGES"] Rails.application.initialize! # routes diff --git a/test/requests/static_error_pages_test.rb b/test/requests/static_error_pages_test.rb new file mode 100644 index 0000000..f3bd8c8 --- /dev/null +++ b/test/requests/static_error_pages_test.rb @@ -0,0 +1,27 @@ +require 'test_helper' + +class StaticErrorPagesTest < ActionController::TestCase + if ENV["STATIC_ERROR_PAGES"] + test 'the exceptions app is set to PublicExceptions' do + assert_nil Rails.application.config.exceptions_app + end + + test 'the assets:precompile task is enhanced with rambulance:precompile' do + assert_not_empty Rake::Task['assets:precompile'].actions + end + else + test 'the exceptions app is set to PublicExceptions' do + assert_not_nil Rails.application.config.exceptions_app + end + + test 'the assets:precompile task is not enhanced with rambulance:precompile' do + assert_empty Rake::Task['assets:precompile'].actions + end + end + + test 'the exceptions app is mounted on /rambulance' do + route = Rails.application.routes.routes.find { |route| route.path.spec.to_s == '/rambulance' } + + assert_not_nil route + end +end From 71be37ae04c801248c40d3a1f968b7ec90238964 Mon Sep 17 00:00:00 2001 From: Yuki Nishijima Date: Mon, 2 Oct 2023 22:17:26 +0900 Subject: [PATCH 7/7] Test against Rails 7.1.0.rc2 --- Appraisals | 6 +++--- gemfiles/rails_71.gemfile | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Appraisals b/Appraisals index acf8f18..2a1c48b 100644 --- a/Appraisals +++ b/Appraisals @@ -44,9 +44,9 @@ appraise "rails_70" do end appraise "rails_71" do - gem "activesupport", "~> 7.1.0.beta1" - gem "actionpack", "~> 7.1.0.beta1" - gem "railties", "~> 7.1.0.beta1" + gem "activesupport", "~> 7.1.0.rc2" + gem "actionpack", "~> 7.1.0.rc2" + gem "railties", "~> 7.1.0.rc2" end appraise "rails_edge" do diff --git a/gemfiles/rails_71.gemfile b/gemfiles/rails_71.gemfile index 83b9409..a18a7b5 100644 --- a/gemfiles/rails_71.gemfile +++ b/gemfiles/rails_71.gemfile @@ -2,8 +2,8 @@ source "https://rubygems.org" -gem "activesupport", "~> 7.1.0.beta1" -gem "actionpack", "~> 7.1.0.beta1" -gem "railties", "~> 7.1.0.beta1" +gem "activesupport", "~> 7.1.0.rc2" +gem "actionpack", "~> 7.1.0.rc2" +gem "railties", "~> 7.1.0.rc2" gemspec path: "../"