aboutsummaryrefslogblamecommitdiffstats
path: root/actionpack/test/dispatch/debug_exceptions_test.rb
blob: c85476fa38c94369bba107c8f5b12837f538034b (plain) (tree)
1
2
3
4
5
6
7
8
9
10
                             
 
                       

                                                           

                                              
              

                         
                                    
                          


                     

                                                                      




                    

       
                          
                                                  

       
                               

                         
           
                            
            
                           


         
                 
                                                                 
                                            
                                                                                                                                            
 
                   
                  
                                              
                       
                                                
                           
                          
                                
                                                
                               
                                      
                                 
                                                 
                             
                                              
                                  
                                                        
                                          


                                                      
                                                         
           





                                                                      
                              
                                                                                             
                         
                                          
                          
                                                                      
                               
                                                                    
                                   
                                                                                   
                                    
             
                               
                        
                                                         
           
                              
                          
                               
                               





                     

                                                                                  


                                                                                    

                                                                                                                  
 
                                                             

                                
                                                                     


       
                                                    

                                
                                                                      


       
                                              

                                                  
                                                                         


       
                                                    


                                                      
                                                                         



                                                               
                                                                 
                         
                                                                       
                                                  


                                                                                                

     
                                                                         

                         
                                                                       
 

                                  

     


                                           
                                                                   


                              
                                                                            
                       
                                                                    
 
                                                                                     

                                                            
 
                                                                                      


                                                             
                                                                              

                                                      
 
                                                                                    

                                                            

     

                                                  
                                                                                                                
 
                                     
                       
                                     
                                   
                                                    
                                            
 
                                          


                                       

                                                          

       
                                              

                                   
                                                    

                                                                    
                                                       

                                   
                                                    

                                                            
                                                        

                                   
                                                    

                                                             
                                                

                                   
                                                    

                                                      
                                                      

                                   
                                                    


                                                            
                                                       
                                                                                 
 
                                                                              





                                                          
                                                                                       




                                                                    
                                                                                                




                                                            
                                                                                                 




                                                             
                                                                                         




                                                          
                                                                                               





                                                            
                                                        
                                                                                 
 
                                                                             
                       







                                                                                 
 
                                                                            




                                                         
                                                                                      
                                              
 

                                                             
 
                                                                                 
 



                                                                                     
 

                                 

     


                                             
                                                                                              
                                                    



                                                                    
                                                                                     

                         
                                                                                               
                       
                                                             
                                                                                  








                                                                                                   

     


                                                         
                                                                               




                                                              

                                                                                                
                                    




                                                


                       
                                                                    

     



                                 



                        


       
                                    



                                                



                       
                                                                  

     


                                           
                                                                   


                                                                             
                                

                         
                                                                                                                   


                                                      
                                       





                                                                   
                                                                                                         














                                                                                 
















                                                                                                           
                                           
                         

                                                           

                                                                                                                               

                                                    
     
 
                                                            



                                                           
                                                      
                                                          
                                                                     
 
                         
                                                                       
     
 
                                                            

                         
                                                                                                                         

                       

                                                      


       
                                                        

                         
                           




                                                      


                                        
 
                                 

     
                                                                                                   

                         
                                                                                                                          

                       

                                                      
       
                                                                                  
     
 
                                                                                     
                         
                                          



                                                                                     
 
                                                                                          
 

                            
                                              
 
                                                                 

                                                              
         
 
                                                               

                                                                    
         
 
                                                                                      

                                                                                              
         
 
                                                            

                                                          
         

       















                                                                                    


































                                                                                               
   
# frozen_string_literal: true

require "abstract_unit"

class DebugExceptionsTest < ActionDispatch::IntegrationTest
  InterceptedErrorInstance = StandardError.new

  class Boomer
    attr_accessor :closed

    def initialize(detailed = false)
      @detailed = detailed
      @closed = false
    end

    # We're obliged to implement this (even though it doesn't actually
    # get called here) to properly comply with the Rack SPEC
    def each
    end

    def close
      @closed = true
    end

    def method_that_raises
      raise StandardError.new "error in framework"
    end

    def raise_nested_exceptions
      raise "First error"
    rescue
      begin
        raise "Second error"
      rescue
        raise "Third error"
      end
    end

    def call(env)
      env["action_dispatch.show_detailed_exceptions"] = @detailed
      req = ActionDispatch::Request.new(env)
      template = ActionView::Template.new(File.read(__FILE__), __FILE__, ActionView::Template::Handlers::Raw.new, format: :html, locals: [])

      case req.path
      when "/pass"
        [404, { "X-Cascade" => "pass" }, self]
      when "/not_found"
        raise AbstractController::ActionNotFound
      when "/runtime_error"
        raise RuntimeError
      when "/method_not_allowed"
        raise ActionController::MethodNotAllowed
      when "/intercepted_error"
        raise InterceptedErrorInstance
      when "/unknown_http_method"
        raise ActionController::UnknownHttpMethod
      when "/not_implemented"
        raise ActionController::NotImplemented
      when "/unprocessable_entity"
        raise ActionController::InvalidAuthenticityToken
      when "/not_found_original_exception"
        begin
          raise AbstractController::ActionNotFound.new
        rescue
          raise ActionView::Template::Error.new(template)
        end
      when "/cause_mapped_to_rescue_responses"
        begin
          raise ActionController::ParameterMissing, :missing_param_key
        rescue
          raise NameError.new("uninitialized constant Userr")
        end
      when "/missing_template"
        raise ActionView::MissingTemplate.new(%w(foo), "foo/index", %w(foo), false, "mailer")
      when "/bad_request"
        raise ActionController::BadRequest
      when "/missing_keys"
        raise ActionController::UrlGenerationError, "No route matches"
      when "/parameter_missing"
        raise ActionController::ParameterMissing, :missing_param_key
      when "/original_syntax_error"
        eval "broke_syntax =" # `eval` need for raise native SyntaxError at runtime
      when "/syntax_error_into_view"
        begin
          eval "broke_syntax ="
        rescue Exception
          raise ActionView::Template::Error.new(template)
        end
      when "/framework_raises"
        method_that_raises
      when "/nested_exceptions"
        raise_nested_exceptions
      else
        raise "puke!"
      end
    end
  end

  Interceptor = proc { |request, exception| request.set_header("int", exception) }
  BadInterceptor = proc { |request, exception| raise "bad" }
  RoutesApp = Struct.new(:routes).new(SharedTestRoutes)
  ProductionApp  = ActionDispatch::DebugExceptions.new(Boomer.new(false), RoutesApp)
  DevelopmentApp = ActionDispatch::DebugExceptions.new(Boomer.new(true), RoutesApp)
  InterceptedApp = ActionDispatch::DebugExceptions.new(Boomer.new(true), RoutesApp, :default, [Interceptor])
  BadInterceptedApp = ActionDispatch::DebugExceptions.new(Boomer.new(true), RoutesApp, :default, [BadInterceptor])

  test "skip diagnosis if not showing detailed exceptions" do
    @app = ProductionApp
    assert_raise RuntimeError do
      get "/", headers: { "action_dispatch.show_exceptions" => true }
    end
  end

  test "skip diagnosis if not showing exceptions" do
    @app = DevelopmentApp
    assert_raise RuntimeError do
      get "/", headers: { "action_dispatch.show_exceptions" => false }
    end
  end

  test "raise an exception on cascade pass" do
    @app = ProductionApp
    assert_raise ActionController::RoutingError do
      get "/pass", headers: { "action_dispatch.show_exceptions" => true }
    end
  end

  test "closes the response body on cascade pass" do
    boomer = Boomer.new(false)
    @app = ActionDispatch::DebugExceptions.new(boomer)
    assert_raise ActionController::RoutingError do
      get "/pass", headers: { "action_dispatch.show_exceptions" => true }
    end
    assert boomer.closed, "Expected to close the response body"
  end

  test "displays routes in a table when a RoutingError occurs" do
    @app = DevelopmentApp
    get "/pass", headers: { "action_dispatch.show_exceptions" => true }
    routing_table = body[/route_table.*<.table>/m]
    assert_match "/:controller(/:action)(.:format)", routing_table
    assert_match ":controller#:action", routing_table
    assert_no_match "&lt;|&gt;", routing_table, "there should not be escaped html in the output"
  end

  test "displays request and response info when a RoutingError occurs" do
    @app = DevelopmentApp

    get "/pass", headers: { "action_dispatch.show_exceptions" => true }

    assert_select "h2", /Request/
    assert_select "h2", /Response/
  end

  test "rescue with diagnostics message" do
    @app = DevelopmentApp

    get "/", headers: { "action_dispatch.show_exceptions" => true }
    assert_response 500
    assert_match(/puke/, body)

    get "/not_found", headers: { "action_dispatch.show_exceptions" => true }
    assert_response 404
    assert_match(/#{AbstractController::ActionNotFound.name}/, body)

    get "/method_not_allowed", headers: { "action_dispatch.show_exceptions" => true }
    assert_response 405
    assert_match(/ActionController::MethodNotAllowed/, body)

    get "/unknown_http_method", headers: { "action_dispatch.show_exceptions" => true }
    assert_response 405
    assert_match(/ActionController::UnknownHttpMethod/, body)

    get "/bad_request", headers: { "action_dispatch.show_exceptions" => true }
    assert_response 400
    assert_match(/ActionController::BadRequest/, body)

    get "/parameter_missing", headers: { "action_dispatch.show_exceptions" => true }
    assert_response 400
    assert_match(/ActionController::ParameterMissing/, body)
  end

  test "rescue with text error for xhr request" do
    @app = DevelopmentApp
    xhr_request_env = { "action_dispatch.show_exceptions" => true, "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest" }

    get "/", headers: xhr_request_env
    assert_response 500
    assert_no_match(/<header>/, body)
    assert_no_match(/<body>/, body)
    assert_equal "text/plain", response.content_type
    assert_match(/RuntimeError\npuke/, body)

    Rails.stub :root, Pathname.new(".") do
      get "/", headers: xhr_request_env

      assert_response 500
      assert_match "Extracted source (around line #", body
      assert_select "pre", { count: 0 }, body
    end

    get "/not_found", headers: xhr_request_env
    assert_response 404
    assert_no_match(/<body>/, body)
    assert_equal "text/plain", response.content_type
    assert_match(/#{AbstractController::ActionNotFound.name}/, body)

    get "/method_not_allowed", headers: xhr_request_env
    assert_response 405
    assert_no_match(/<body>/, body)
    assert_equal "text/plain", response.content_type
    assert_match(/ActionController::MethodNotAllowed/, body)

    get "/unknown_http_method", headers: xhr_request_env
    assert_response 405
    assert_no_match(/<body>/, body)
    assert_equal "text/plain", response.content_type
    assert_match(/ActionController::UnknownHttpMethod/, body)

    get "/bad_request", headers: xhr_request_env
    assert_response 400
    assert_no_match(/<body>/, body)
    assert_equal "text/plain", response.content_type
    assert_match(/ActionController::BadRequest/, body)

    get "/parameter_missing", headers: xhr_request_env
    assert_response 400
    assert_no_match(/<body>/, body)
    assert_equal "text/plain", response.content_type
    assert_match(/ActionController::ParameterMissing/, body)
  end

  test "rescue with JSON error for JSON API request" do
    @app = ActionDispatch::DebugExceptions.new(Boomer.new(true), RoutesApp, :api)

    get "/", headers: { "action_dispatch.show_exceptions" => true }, as: :json
    assert_response 500
    assert_no_match(/<header>/, body)
    assert_no_match(/<body>/, body)
    assert_equal "application/json", response.content_type
    assert_match(/RuntimeError: puke/, body)

    get "/not_found", headers: { "action_dispatch.show_exceptions" => true }, as: :json
    assert_response 404
    assert_no_match(/<body>/, body)
    assert_equal "application/json", response.content_type
    assert_match(/#{AbstractController::ActionNotFound.name}/, body)

    get "/method_not_allowed", headers: { "action_dispatch.show_exceptions" => true }, as: :json
    assert_response 405
    assert_no_match(/<body>/, body)
    assert_equal "application/json", response.content_type
    assert_match(/ActionController::MethodNotAllowed/, body)

    get "/unknown_http_method", headers: { "action_dispatch.show_exceptions" => true }, as: :json
    assert_response 405
    assert_no_match(/<body>/, body)
    assert_equal "application/json", response.content_type
    assert_match(/ActionController::UnknownHttpMethod/, body)

    get "/bad_request", headers: { "action_dispatch.show_exceptions" => true }, as: :json
    assert_response 400
    assert_no_match(/<body>/, body)
    assert_equal "application/json", response.content_type
    assert_match(/ActionController::BadRequest/, body)

    get "/parameter_missing", headers: { "action_dispatch.show_exceptions" => true }, as: :json
    assert_response 400
    assert_no_match(/<body>/, body)
    assert_equal "application/json", response.content_type
    assert_match(/ActionController::ParameterMissing/, body)
  end

  test "rescue with HTML format for HTML API request" do
    @app = ActionDispatch::DebugExceptions.new(Boomer.new(true), RoutesApp, :api)

    get "/index.html", headers: { "action_dispatch.show_exceptions" => true }
    assert_response 500
    assert_match(/<header>/, body)
    assert_match(/<body>/, body)
    assert_equal "text/html", response.content_type
    assert_match(/puke/, body)
  end

  test "rescue with XML format for XML API requests" do
    @app = ActionDispatch::DebugExceptions.new(Boomer.new(true), RoutesApp, :api)

    get "/index.xml", headers: { "action_dispatch.show_exceptions" => true }
    assert_response 500
    assert_equal "application/xml", response.content_type
    assert_match(/RuntimeError: puke/, body)
  end

  test "rescue with JSON format as fallback if API request format is not supported" do
    Mime::Type.register "text/wibble", :wibble

    ActionDispatch::IntegrationTest.register_encoder(:wibble,
      param_encoder: -> params { params })

    @app = ActionDispatch::DebugExceptions.new(Boomer.new(true), RoutesApp, :api)

    get "/index", headers: { "action_dispatch.show_exceptions" => true }, as: :wibble
    assert_response 500
    assert_equal "application/json", response.content_type
    assert_match(/RuntimeError: puke/, body)

  ensure
    Mime::Type.unregister :wibble
  end

  test "does not show filtered parameters" do
    @app = DevelopmentApp

    get "/", params: { "foo" => "bar" }, headers: { "action_dispatch.show_exceptions" => true,
      "action_dispatch.parameter_filter" => [:foo] }
    assert_response 500
    assert_match("&quot;foo&quot;=&gt;&quot;[FILTERED]&quot;", body)
  end

  test "show registered original exception if the last exception is TemplateError" do
    @app = DevelopmentApp

    get "/not_found_original_exception", headers: { "action_dispatch.show_exceptions" => true }
    assert_response 404
    assert_match %r{AbstractController::ActionNotFound}, body
    assert_match %r{Showing <i>.*test/dispatch/debug_exceptions_test.rb</i>}, body
  end

  test "show the last exception and cause even when the cause is mapped to resque_responses" do
    @app = DevelopmentApp

    get "/cause_mapped_to_rescue_responses", headers: { "action_dispatch.show_exceptions" => true }
    assert_response 500
    assert_match %r{ActionController::ParameterMissing}, body
    assert_match %r{NameError}, body
  end

  test "named urls missing keys raise 500 level error" do
    @app = DevelopmentApp

    get "/missing_keys", headers: { "action_dispatch.show_exceptions" => true }
    assert_response 500

    assert_match(/ActionController::UrlGenerationError/, body)
  end

  test "show the controller name in the diagnostics template when controller name is present" do
    @app = DevelopmentApp
    get("/runtime_error", headers: {
      "action_dispatch.show_exceptions" => true,
      "action_dispatch.request.parameters" => {
        "action" => "show",
        "id" => "unknown",
        "controller" => "featured_tile"
      }
    })
    assert_response 500
    assert_match(/RuntimeError\n\s+in FeaturedTileController/, body)
  end

  test "show formatted params" do
    @app = DevelopmentApp

    params = {
      "id" => "unknown",
      "someparam" => {
        "foo" => "bar",
        "abc" => "goo"
      }
    }

    get("/runtime_error", headers: {
      "action_dispatch.show_exceptions" => true,
      "action_dispatch.request.parameters" => {
        "action" => "show",
        "controller" => "featured_tile"
      }.merge(params)
    })
    assert_response 500

    assert_includes(body, CGI.escapeHTML(PP.pp(params, +"", 200)))
  end

  test "sets the HTTP charset parameter" do
    @app = DevelopmentApp

    get "/", headers: { "action_dispatch.show_exceptions" => true }
    assert_equal "text/html; charset=utf-8", response.headers["Content-Type"]
  end

  test "uses logger from env" do
    @app = DevelopmentApp
    output = StringIO.new
    get "/", headers: { "action_dispatch.show_exceptions" => true, "action_dispatch.logger" => Logger.new(output) }
    assert_match(/puke/, output.rewind && output.read)
  end

  test "logs only what is necessary" do
    @app = DevelopmentApp
    io = StringIO.new
    logger = ActiveSupport::Logger.new(io)

    _old, ActionView::Base.logger = ActionView::Base.logger, logger
    begin
      get "/", headers: { "action_dispatch.show_exceptions" => true, "action_dispatch.logger" => logger }
    ensure
      ActionView::Base.logger = _old
    end

    output = io.rewind && io.read
    lines = output.lines

    # Other than the first three...
    assert_equal(["  \n", "RuntimeError (puke!):\n", "  \n"], lines.slice!(0, 3))
    lines.each do |line|
      # .. all the remaining lines should be from the backtrace
      assert_match(/:\d+:in /, line)
    end
  end

  test "logs with non active support loggers" do
    @app = DevelopmentApp
    io = StringIO.new
    logger = Logger.new(io)

    _old, ActionView::Base.logger = ActionView::Base.logger, logger
    begin
      assert_nothing_raised do
        get "/", headers: { "action_dispatch.show_exceptions" => true, "action_dispatch.logger" => logger }
      end
    ensure
      ActionView::Base.logger = _old
    end

    assert_match(/puke/, io.rewind && io.read)
  end

  test "uses backtrace cleaner from env" do
    @app = DevelopmentApp
    backtrace_cleaner = ActiveSupport::BacktraceCleaner.new

    backtrace_cleaner.stub :clean, ["passed backtrace cleaner"] do
      get "/", headers: { "action_dispatch.show_exceptions" => true, "action_dispatch.backtrace_cleaner" => backtrace_cleaner }
      assert_match(/passed backtrace cleaner/, body)
    end
  end

  test "logs exception backtrace when all lines silenced" do
    output = StringIO.new
    backtrace_cleaner = ActiveSupport::BacktraceCleaner.new
    backtrace_cleaner.add_silencer { true }

    env = { "action_dispatch.show_exceptions" => true,
           "action_dispatch.logger" => Logger.new(output),
           "action_dispatch.backtrace_cleaner" => backtrace_cleaner }

    get "/", headers: env
    assert_operator((output.rewind && output.read).lines.count, :>, 10)
  end

  test "display backtrace when error type is SyntaxError" do
    @app = DevelopmentApp

    get "/original_syntax_error", headers: { "action_dispatch.backtrace_cleaner" => ActiveSupport::BacktraceCleaner.new }

    assert_response 500
    assert_select "#Application-Trace-0" do
      assert_select "code", /syntax error, unexpected/
    end
  end

  test "display backtrace on template missing errors" do
    @app = DevelopmentApp

    get "/missing_template"

    assert_select "header h1", /Template is missing/

    assert_select "#container h2", /^Missing template/

    assert_select "#Application-Trace-0"
    assert_select "#Framework-Trace-0"
    assert_select "#Full-Trace-0"

    assert_select "h2", /Request/
  end

  test "display backtrace when error type is SyntaxError wrapped by ActionView::Template::Error" do
    @app = DevelopmentApp

    get "/syntax_error_into_view", headers: { "action_dispatch.backtrace_cleaner" => ActiveSupport::BacktraceCleaner.new }

    assert_response 500
    assert_select "#Application-Trace-0" do
      assert_select "code", /syntax error, unexpected/
    end
    assert_match %r{Showing <i>.*test/dispatch/debug_exceptions_test.rb</i>}, body
  end

  test "debug exceptions app shows user code that caused the error in source view" do
    @app = DevelopmentApp
    Rails.stub :root, Pathname.new(".") do
      cleaner = ActiveSupport::BacktraceCleaner.new.tap do |bc|
        bc.add_silencer { |line| line =~ /method_that_raises/ }
        bc.add_silencer { |line| line !~ %r{test/dispatch/debug_exceptions_test.rb} }
      end

      get "/framework_raises", headers: { "action_dispatch.backtrace_cleaner" => cleaner }

      # Assert correct error
      assert_response 500
      assert_select "h2", /error in framework/

      # assert source view line is the call to method_that_raises
      assert_select "div.source:not(.hidden)" do
        assert_select "pre .line.active", /method_that_raises/
      end

      # assert first source view (hidden) that throws the error
      assert_select "div.source:first" do
        assert_select "pre .line.active", /raise StandardError\.new/
      end

      # assert application trace refers to line that calls method_that_raises is first
      assert_select "#Application-Trace-0" do
        assert_select "code a:first", %r{test/dispatch/debug_exceptions_test\.rb:\d+:in `call}
      end

      # assert framework trace that threw the error is first
      assert_select "#Framework-Trace-0" do
        assert_select "code a:first", /method_that_raises/
      end
    end
  end

  test "invoke interceptors before rendering" do
    @app = InterceptedApp
    get "/intercepted_error", headers: { "action_dispatch.show_exceptions" => true }

    assert_equal InterceptedErrorInstance, request.get_header("int")
  end

  test "bad interceptors doesn't debug exceptions" do
    @app = BadInterceptedApp

    get "/puke", headers: { "action_dispatch.show_exceptions" => true }

    assert_response 500
    assert_match(/puke/, body)
  end

  test "debug exceptions app shows all the nested exceptions in source view" do
    @app = DevelopmentApp
    Rails.stub :root, Pathname.new(".") do
      cleaner = ActiveSupport::BacktraceCleaner.new.tap do |bc|
        bc.add_silencer { |line| line !~ %r{test/dispatch/debug_exceptions_test.rb} }
      end

      get "/nested_exceptions", headers: { "action_dispatch.backtrace_cleaner" => cleaner }

      # Assert correct error
      assert_response 500
      assert_select "h2", /Third error/

      # assert source view line shows the last error
      assert_select "div.source:not(.hidden)" do
        assert_select "pre .line.active", /raise "Third error"/
      end

      # assert application trace refers to line that raises the last exception
      assert_select "#Application-Trace-0" do
        assert_select "code a:first", %r{in `rescue in rescue in raise_nested_exceptions'}
      end

      # assert the second application trace refers to the line that raises the second exception
      assert_select "#Application-Trace-1" do
        assert_select "code a:first", %r{in `rescue in raise_nested_exceptions'}
      end

      # assert the third application trace refers to the line that raises the first exception
      assert_select "#Application-Trace-2" do
        assert_select "code a:first", %r{in `raise_nested_exceptions'}
      end
    end
  end
end