I'm a Ruby on Rails / jQuery web developer. Follow me at @sikachu

Ruby และ active_support/whiny_nil

June 29th, 2009 Posted in My Project, Programming, Ruby, Ruby on Rails

สำหรับนักพัฒนาส่วนใหญ่ที่เริ่มเขียน Ruby on Rails คาดว่าตอนนี้ในเครื่องของทุกๆ คน น่าจะลง Ruby 1.8 อยู่ เนื่องจากยังคงมี Gem หลายๆ ตัว ที่ยังไม่รองรับ Ruby 1.9 และทำให้เกิดปัญหาทางด้านความเข้ากันได้อยู่บ้าง ฉะนั้นผมเลยอยากพูดถึงหลุมพรางที่ Ruby 1.8 ได้ทิ้งเอาไว้ และทำให้หลายๆ คนนั้นพลาดตกหลุมกันไปบ้างครับ

ผมขอสมมุติเอาไว้ว่า ผมได้สร้างระบบ Blog แห่งหนึ่ง โดยที่มี Model สามตัวคือ Post เอาไว้เก็บข้อความ Comment เอาไว้เก็บความคิดเห็น และ User เอาไว้เก็บชื่อผู้ใช้ ที่สามารถแก้ไขข้อความได้ครับ

สมมุติว่า User ที่สามารถเข้ามาแก้ไขได้นี้ มี id = 4 ผมก็เลยทำการ hard-coded เอาไว้ในโปรแกรมเลย เป็น filter ว่า

class PostController < ApplicationController
  before_filter :load_user
  before_filter :check_authorization!, :except => [:show, :index]
 
  # ... controller actions
 
  private
 
  def check_authorization!
    render :text => "Unauthorized!", :status => 401 unless @user.id == 4
  end
end

คราวนี้ผมก็ลองรันดู ปรากฎว่าเจอปัญหาว่าถ้าคนที่ไม่ได้เป็นสมาชิกเข้ามาที่บล็อก มันจะเกิด exception ขึ้นมา เพราะผมไปเรียก id method บน nil object ผมก็เลยจัดการ catch exception อย่างนี้ครับ

  def check_authorization!
    render :text => "Unauthorized!", :status => 401 unless (@user.id == 4 rescue false)
  end

โออ คราวนี้โปรแกรมผมหล่อกิ๊งเลย ไม่พบว่าเกิดปัญหาอะไร ผมก็เลยจัดการ deploy ขึ้นไปบนเว็บเสร็จสรรพ สร้าง user ให้เหมือนกับบนเครื่อง Development ทุกประการ และให้ User ที่มี id = 4 สามารถแก้ไขโพสได้เป็นคนเดียวเช่นเคย

แต่ปรากฎว่า การเขียนโปรแกรมของผมนั้นทิ้งช่องโหว่เอาไว้ใหญ่โตเลยครับ เพราะกลายเป็นว่าคนที่ไม่ได้เป็นสมาชิกสามารถแก้ไข และลบข้อความทั้งหมดของผมได้เลย !!

หลังจากการตามหาบักมานานแสนนาน .. ผมก็ขอเข้าเรื่องของ whiny_nil เลยละกันครับ

ในตัว active_support ที่ติดมากับ Ruby on Rails นั้น มีไฟล์อยู่อันหนึ่งชื่อว่า whiny_nil.rb ซึ่งไฟล์นี้เป็นส่วนของ Core Extension ซึ่งทำให้การเรียกใช้เมธอด #id บน object ที่เป็น nil นั้น จะมีการโยน RuntimeError ออกมาบอกว่าเรากำลังทำการเรียกใช้ #id บน nil อยู่

Sikachus-Notebook:rails sikachu$ script/console
Loading development environment (Rails 2.3.2)
>> nil.id
RuntimeError: Called id for nil, which would mistakenly be 4 -- if you really wanted the id of nil, use object_id
	from (irb):1
>>

ซึ่งตรงนี้ นักพัฒนาบางคนก็จะใช้วิธีการ rescue RuntimeError ไป เพราะคิดเอาว่าถ้าเผลอไปเรียก #id บน object ที่เป็น nil จริง มันก็ต้องโยน RuntimeError ออกมาบอกเรา ถูกไหมครับ

คำตอบคือ ผิดถนัด ครับ เพราะบน Production environment นั้น เจ้าตัว whiny_nil นั้นจะไม่ถูกเปิดใช้ครับ แล้วผลของการไม่ได้เปิดใช้หรอครับ?

Sikachus-Notebook:~ sikachu$ irb
irb(main):001:0> nil.id
(irb):1: warning: Object#id will be deprecated; use Object#object_id
=> 4

นั่นแหละครับ! การที่เรียกเมธอด #id บน nil นั้น Ruby จะคืนค่าเป็น 4 ครับ เพราะว่า nil นั้นมี id ของตัวมันเองคือ 4 ครับ :)

เพราะฉะนั้นคงเดาออกใช่ไหมครับว่าทำไมคนที่ไม่ได้ล็อกอินทุกคน ถึงสามารถเข้าไปแก้ไขบล็อกของผมได้? ก็เพราะว่า @user.id ของเขา คืนค่าเป็น 4 กันทุกคนเลยครับ ไม่ได้มีการโยน RuntimeError แต่อย่างใด

ปัญหานี้ใช่ว่าไม่มีทางแก้ครับ แต่ผมจะไม่ขอเจาะลึกลงไปแล้วกันครับ เพราะว่าปัญหานี้หายไปแล้วใน Ruby 1.9.1 (object#id deprecated ไปแล้วครับ) ก็ต้องรอให้มันถูกใช้แพร่หลายเท่านั้นล่ะครับ ซึ่งตอนนี้วิธีการแก้ก็คงเป็น

  • เปิดการใช้งาน config.whiny_nil ใน production.rb
  • ใช้ object.try(:id) ที่จะคืนค่ามาเป็น nil หากว่า object เป็น nil
  • เขียนโค้ดใหม่โดยพยายามไม่เช็คจาก #id

หวังว่าโพสนี้จะทำให้หลายคนหายข้องใจได้บ้างนะครับ

ปล. บางท่านคงสงสัยใช้ไหมครับว่าแล้วทำไม nil.id หรือ nil.object_id มันถึง return เป็น 4 … เพราะว่า Ruby ทุกอย่างมันเป็น object ครับ ไม่เว้นแม่กระทั่ง nil! ไม่เชื่อลองดูนี่นะครับ

irb(main):002:0> nil.object_id
=> 4
irb(main):003:0> nil.class
=> NilClass
irb(main):004:0> 100.class
=> Fixnum

ใครจะชอบไม่ชอบผมไม่รู้ แต่ขอเอวังด้วยประกาลฉะนี้ :)

blog comments powered by Disqus