Slide 1

Slide 1 text

rescueされない例外?! #megurorb @suusan2go 2018/9/27 Meguro.rb #19 1

Slide 2

Slide 2 text

● M3, Inc. Software Engineer ● 半年くらいKotlin書いてましたが最近はPythonと Railsで機械学習系のアプリの機械学習以外を全部 やる仕事をしています ● 今日はRailsの話をします! 自己紹介: @suusan2go 2

Slide 3

Slide 3 text

突然ですが開発環境でこのコントローラにアクセスすると 何のエラーが表示されるでしょう 3 class HogeController < ApplicationController def hoge raise 'hogehoge' rescue raise 'piyopiyo' end end

Slide 4

Slide 4 text

答え piyopiyo 4

Slide 5

Slide 5 text

じゃあこれだったら? 5 def fuga raise ActiveRecord::RecordInvalid rescue raise 'piyopiyo' end

Slide 6

Slide 6 text

6

Slide 7

Slide 7 text

7

Slide 8

Slide 8 text

8

Slide 9

Slide 9 text

れ、rescueされてないだと・・・ 9

Slide 10

Slide 10 text

10

Slide 11

Slide 11 text

11 というやりとりをTwitterでみかけて、 気になったので調べてみた話です

Slide 12

Slide 12 text

rails consoleで試してみる 12 irb(main):001:0> def fuga irb(main):002:1> raise ActiveRecord::RecordInvalid irb(main):003:1> rescue irb(main):004:1> raise 'piyopiyo' irb(main):005:1> end => :fuga irb(main):006:0> fuga Traceback (most recent call last): 3: from (irb):6 2: from (irb):1:in `fuga' 1: from (irb):4:in `rescue in fuga' RuntimeError (piyopiyo) irb(main):007:0>

Slide 13

Slide 13 text

binding.pryを仕込んでみる 13 def fuga raise ActiveRecord::RecordInvalid rescue binding.pry raise 'piyopiyo' end

Slide 14

Slide 14 text

rescueされてる箇所にとまる!つまりrescueはされてるっぽい 14 8: def fuga 9: raise ActiveRecord::RecordInvalid 10: rescue 11: binding.pry => 12: raise 'piyopiyo' 13: end

Slide 15

Slide 15 text

15 rescueはされてるっぽい つまり表示の問題? エラー表示箇所のコードを 見てみよう

Slide 16

Slide 16 text

actionpack/lib/action_dispatch/middleware/debug_exceptions.rb 16 def call(env) request = ActionDispatch::Request.new env _, headers, body = response = @app.call(env) if headers["X-Cascade"] == "pass" body.close if body.respond_to?(:close) raise ActionController::RoutingError, "No route matches [#{env['REQUEST_METHOD']}] #{env['PATH_INFO'].inspect}" end response rescue Exception => exception invoke_interceptors(request, exception) raise exception unless request.show_exceptions? render_exception(request, exception) end エラー画面を表示して いる箇所はここ! exceptionはそのまま 渡している

Slide 17

Slide 17 text

actionpack/lib/action_dispatch/middleware/debug_exceptions.rb 17 def render_exception(request, exception) backtrace_cleaner = request.get_header("action_dispatch.backtrace_cleaner") wrapper = ExceptionWrapper.new(backtrace_cleaner, exception) log_error(request, wrapper) if request.get_header("action_dispatch.show_detailed_exceptions") content_type = request.formats.first if api_request?(content_type) render_for_api_request(content_type, wrapper) else render_for_browser_request(request, wrapper) end else raise exception end end ExceptionWrapper 怪しい

Slide 18

Slide 18 text

actionpack/lib/action_dispatch/middleware/exception_wrapper.rb 18 module ActionDispatch class ExceptionWrapper # 〜〜〜省略〜〜〜〜 def initialize(backtrace_cleaner, exception) @backtrace_cleaner = backtrace_cleaner @exception = original_exception(exception) expand_backtrace if exception.is_a?(SyntaxError) || exception.cause.is_a?(SyntaxError) end # 〜〜〜省略〜〜〜〜 end end original_exception!

Slide 19

Slide 19 text

actionpack/lib/action_dispatch/middleware/exception_wrapper.rb 19 def original_exception(exception) if @@rescue_responses.has_key?(exception.cause.class.name) exception.cause else exception end end exception.causeとは

Slide 20

Slide 20 text

https://docs.ruby-lang.org/ja/latest/method/Exception/i/cause.html 20

Slide 21

Slide 21 text

@@rescue_responsesはなにかというと 21 module ActionDispatch class ExceptionWrapper # 〜〜〜省略〜〜〜〜 @@rescue_responses.merge!( "ActionController::RoutingError" => :not_found, "AbstractController::ActionNotFound" => :not_found, "ActionController::MethodNotAllowed" => :method_not_allowed, "ActionController::UnknownHttpMethod" => :method_not_allowed, "ActionController::NotImplemented" => :not_implemented, "ActionController::UnknownFormat" => :not_acceptable, "ActionController::InvalidAuthenticityToken" => :unprocessable_entity, "ActionController::InvalidCrossOriginRequest" => :unprocessable_entity, "ActionDispatch::Http::Parameters::ParseError" => :bad_request, "ActionController::BadRequest" => :bad_request, "ActionController::ParameterMissing" => :bad_request, "Rack::QueryParser::ParameterTypeError" => :bad_request, "Rack::QueryParser::InvalidParameterError" => :bad_request )

Slide 22

Slide 22 text

activerecord/lib/active_record/railtie.rb 22 力尽きてちゃんとコード読んでないんですけど、多分ここで追加されたやつが追加 されてるっぽい config.action_dispatch.rescue_responses.merge!( "ActiveRecord::RecordNotFound" => :not_found, "ActiveRecord::StaleObjectError" => :conflict, "ActiveRecord::RecordInvalid" => :unprocessable_entity, "ActiveRecord::RecordNotSaved" => :unprocessable_entity )

Slide 23

Slide 23 text

結論 23 ● 例外が発生した場合、その例外の根本の例外がcauseでチェックされる ● 根本の例外がrescue_responsesに含まれる場合には画面には最終的 にraiseされた例外ではなく、根本の例外が表示される

Slide 24

Slide 24 text

再考 24 def fuga raise ActiveRecord::RecordInvalid rescue raise 'piyopiyo' end

Slide 25

Slide 25 text

再考 25 def fuga raise ActiveRecord::RecordInvalid rescue raise 'piyopiyo' end 最終的にraiseされているのはこれ しかしエラー表示の際には、この例外の cause である ActiveRecord::RecordInvalidが表示さ れる

Slide 26

Slide 26 text

26 なんでこんな挙動になっているのだ ろう?

Slide 27

Slide 27 text

27 https://github.com/rails/rails/commit/dde54f00c6c08d1704d011d8108106076e8ea033

Slide 28

Slide 28 text

なんと2010年のコミット #5784 はGitHubではなくlighthouseapp 28 https://rails.lighthouseapp.com/projects/8994-ruby-on-rails/tickets/5784-show exceptions-renders-original-exceptions

Slide 29

Slide 29 text

まとめ 29 ● この挙動はちょっとぎょっとしますが仕様です ● 試してみたら、templateでのエラーは ActionView::Template::Errorで Wrapされる ● このcauseを見に行く仕様は、templateでおもむろに `User.find(:id)` と かやってActiveRecord::RecordNotFoundがraiseされても、ちゃんと 404を返したりするためのものっぽい・・・・・・? ● もともとの用途がtemplateでのエラーのためのものなので、causeを見 る挙動はActionView::Template::Errorに絞ってもよいのではという気が してきた。 ● なんか間違ってるところあれば教えてください :pray: