Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add the ability to precompile error pages on asset precompile #70

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ tmp
*.a
mkmf.log
*.log
test/fake_app/public/*.html
6 changes: 3 additions & 3 deletions Appraisals
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
7 changes: 6 additions & 1 deletion Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
6 changes: 3 additions & 3 deletions gemfiles/rails_71.gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -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: "../"
13 changes: 13 additions & 0 deletions lib/rambulance/exceptions_app.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
66 changes: 42 additions & 24 deletions lib/rambulance/railtie.rb
Original file line number Diff line number Diff line change
@@ -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
Expand Down
12 changes: 12 additions & 0 deletions lib/tasks/rambulance.rake
Original file line number Diff line number Diff line change
@@ -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
19 changes: 18 additions & 1 deletion test/exceptions_app_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Empty file added test/fake_app/public/.keep
Empty file.
6 changes: 6 additions & 0 deletions test/fake_app/rails_app.rb
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down
27 changes: 27 additions & 0 deletions test/requests/static_error_pages_test.rb
Original file line number Diff line number Diff line change
@@ -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
Loading