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/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/README.md b/README.md index 0894ae3..4561985 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 = Rails.env.production? +``` + +### 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: 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/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: "../" 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..b5d805e 100644 --- a/lib/rambulance/railtie.rb +++ b/lib/rambulance/railtie.rb @@ -1,36 +1,54 @@ 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" 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 && Rake::Task.task_defined?("assets:precompile") + Rake::Task["assets:precompile"].enhance do + Rake::Task["rambulance:precompile"].invoke + 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..0d376eb 100644 --- a/test/exceptions_app_test.rb +++ b/test/exceptions_app_test.rb @@ -2,8 +2,25 @@ 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 + + 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) + 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 end diff --git a/test/fake_app/public/.keep b/test/fake_app/public/.keep new file mode 100644 index 0000000..e69de29 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