Search code examples
ruby-on-railspumax-frame-options

How to set HTTP headers for 404 Not found pages with Rails/Puma


I need to set the X-Frame-Options HTTP header for pages that return 404 - Not Found in my rails app but I can't figure out how to do it. I am not able to set these headers using rails, I found one possible reason here. However, I don't know how I could set them with the Webserver either, I'm using Puma.

I don't actually have anything that can be ClickJacked in my 404 - not found pages but an external security org still requires me to do so.


Solution

  • In Rails the exceptions are handled by config.exceptions_app. The default app just renders the static html files from the public directory but this can be any rack compliant application.

    The most basic example of a Rack compliant application is:

    app = ->(env){  [ 404, { "Content-Type" => "text/plain", "X-Frame-Options" => "some value" }, ["Oh no I cant find it!"] ] }
    

    It takes one argument (A hash) and returns an array(status, headers, body).

    Both the Rails routes and ActionController::Metal (and thus all your controllers) are rack compliant applications and even config/application.rb. In fact Rails is just a russian doll scheme of Rack apps.

    If you want to handle this through your routes you can do:

    # config/application.rb
    config.exceptions_app = self.routes
    
    # config/routes.rb
    get '/404', to: "errors#not_found"
    get '/422', to: "errors#unprocessable_entity"
    get '/500', to: "errors#internal_error"
    
    class ErrorsController < ActionController::Base
      before_action do
        response.set_header('X-Frame-Options', 'HEADER VALUE')
      end
    
      def not_found
        respond_to do |format|
          format.html { render file: Rails.root.join('public', '404.html'), layout: false, status: :not_found }
        end
      end
    
      def unprocessable_entity 
        respond_to do |format|
          format.html { render file: Rails.root.join('public', '422.html'), layout: false, status: :unprocessable_entity }
        end
      end
    
      def internal_error
        respond_to do |format|
          format.html { render file: Rails.root.join('public', '500.html'), layout: false, status: :internal_server_error }
        end
      end
    end