สิ่งดีของ Rails ตอน development something goes wrong ได้ error detail page สวยงาม Backtrace สวย ส่วน relevant ไป app highlighted ดู params ที่ posted, inspect environment และ session variables
วันนี้ดูว่ fancy error pages นี้ทำงานยังไง

Cracking Open Actionpack
File สำคัญวันนี้: actionpack/lib/action_dispatch/middleware/debug_exceptions.rb ทำ heavy lifting สำหรับ development mode error pages Production mode error screens ดูที่ public_exceptions.rb
Rack Middleware
ไม่คุ้น rack middleware concept 簡單: intercept HTTP requests ก่อนถึง app และ intercept app output ก่อนกลับ user
Middleware 简单 ไม่ทำอะไรน่าสนใจ:
class MyMiddleware def initialize(app) @app = app end def call(env) @app.call(env) end end
Rescuing All Exceptions via Rack Middleware
Exception ใน app เกิดจากการเรียก @app.call() Rescue all exceptions ใน rack app ง่ายแค่เพิ่ม rescue clause ใน middleware
def call(env) @app.call(env) rescue StandardError => exception # this is a method we have to provide to generate the exception page render_exception(env, exception) end
อะไรก็ตามที่คืนจาก call method จะถูก treat เหมือน normal web page Content จาก render_exception แทนที่ original response
Rendering the Exception
Excerpt render_exception method จาก ActionDispatch::DebugExceptions ดูว่าแค่ pull relevant data จาก exception feed เข้า ERB template
def render_exception(env, exception) wrapper = ExceptionWrapper.new(env, exception) log_error(env, wrapper) if env['action_dispatch.show_detailed_exceptions'] request = Request.new(env) template = ActionView::Base.new([RESCUES_TEMPLATE_PATH], request: request, exception: wrapper.exception, application_trace: wrapper.application_trace, framework_trace: wrapper.framework_trace, full_trace: wrapper.full_trace, routes_inspector: routes_inspector(exception), source_extract: wrapper.source_extract, line_number: wrapper.line_number, file: wrapper.file ) file = "rescues/#{wrapper.rescue_template}" if request.xhr? body = template.render(template: file, layout: false, formats: [:text]) format = "text/plain" else body = template.render(template: file, layout: 'rescues/layout') format = "text/html" end render(wrapper.status_code, body, format) else raise exception end end def render(status, body, format) [status, {'Content-Type' => "#{format}; charset=#{Response.default_charset}", 'Content-Length' => body.bytesize.to_s}, [body]] end
Other Uses
Rack middleware trick ใช้ทำ interesting things กับ exceptions ได้ ที่ Honeybadger ใช้ intercept errors record ไป API โค้ด:
def call(env) config.with_request(::Rack::Request.new(env)) do begin env['honeybadger.config'] = config response = @app.call(env) rescue Exception => raised env['honeybadger.error_id'] = notify_honeybadger(raised, env) raise end framework_exception = framework_exception(env) if framework_exception env['honeybadger.error_id'] = notify_honeybadger(framework_exception, env) end response end ensure Honeybadger.context.clear! end