Upgrade to Pro — share decks privately, control downloads, hide ads and more …

rescue されない例外?! / A Exception not rescue #megurorb

Kenta Suzuki
September 27, 2018

rescue されない例外?! / A Exception not rescue #megurorb

Meguro.rb #19 で話しました
https://megurorb.connpass.com/event/100401/

Kenta Suzuki

September 27, 2018
Tweet

More Decks by Kenta Suzuki

Other Decks in Programming

Transcript

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

    View Slide

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

    View Slide

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

    View Slide

  4. 答え piyopiyo
    4

    View Slide

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

    View Slide

  6. 6

    View Slide

  7. 7

    View Slide

  8. 8

    View Slide

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

    View Slide

  10. 10

    View Slide

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

    View Slide

  12. 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>

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  16. 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はそのまま
    渡している

    View Slide

  17. 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
    怪しい

    View Slide

  18. 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!

    View Slide

  19. 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とは

    View Slide

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

    View Slide

  21. @@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
    )

    View Slide

  22. 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
    )

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide