บทเรียน 104 · Rails · Day 1

หยุดดูข้างใน — debugger และ bin/rails console

~20 นาที · ควรผ่าน บทเรียน 102 (controller · params) และ บทเรียน 103 (view · helper) มาก่อน

ปัญหาที่ทุกคนเจอ: โค้ดไม่ทำงานตามที่คิด แต่มองไม่เห็นว่าข้างในมีค่าอะไร
อ่านจบบทนี้ คุณจะมีเครื่องมือสองชิ้นไว้ "มองเข้าไปข้างใน": debugger สั่งให้โค้ดหยุดกลางทางแล้วถามค่าอะไรก็ได้ · และ bin/rails console เปิดแอปขึ้นมาให้ลองพิมพ์คำสั่งเล่นได้ทีละบรรทัด โดยไม่ต้องเปิดเว็บ
เราจะใช้ทั้งสองอย่างไล่หาบั๊กจริงหนึ่งตัว — ตัวอักษรสลับที่แค่สองตัว ที่ทำให้หน้าเว็บทักทายไม่มีชื่อ

🔁 ทวนก่อนเริ่ม — คำจากบทก่อน ๆ ที่บทนี้ใช้

🧰 ตรวจด้วย Ruby 4.0.9 · Rails 8.1.4 · gem debug (rdbg 1.11.1) (สไลด์ใช้ Ruby 4.0.6 · Rails 8.1.3 — รุ่นแก้บั๊กในสายเดียวกัน ใช้แทนกันได้)

ผลทุกก้อนในบทนี้รันจริงแล้วก็อปมา · จัดแค่ย่อ path ยาวเป็น ~/wongnok_blog และตัดบรรทัดที่ไม่เกี่ยวเป็น … · เวลา (ms) · วันที่ · เลข 0x… ของคุณจะไม่ตรงกับในบท และจอคุณจะมีสี

ปัญหา: หน้าเว็บทักทายแต่ไม่มีชื่อ

เริ่มจากแอปเล็ก ๆ ที่ใช้คำจากบท 101–103 ครบทุกตัว — หน้า /hello ที่ทักทายตามชื่อในใบสั่ง
ถ้าเปิด /hello?name=Somchai ควรได้หน้า "สวัสดีคุณ Somchai!"

# config/routes.rb — พนักงานต้อนรับ: ใบสั่ง GET /hello ส่งไปแผนก pages เมนู hello
Rails.application.routes.draw do
  get "/hello", to: "pages#hello"
end
# app/controllers/pages_controller.rb — หัวหน้าเชฟแผนก pages
class PagesController < ApplicationController
  def hello
    @name = params[:nmae]
    @message = "สวัสดีคุณ #{@name}!"
  end
end
<%# app/views/pages/hello.html.erb — จัดจาน: เอา @message มาวางในหัวข้อ %>
<h1><%= @message %></h1>

ทวนทีละบรรทัด:

# เปิด bin/rails server แล้วเข้าเบราว์เซอร์ที่
http://localhost:3000/hello?name=Somchai

หัวข้อบนหน้าเว็บจะเขียนว่าอะไร? แล้วจะมี error ขึ้นไหม?

ผลจริง (ดึง HTML ด้วย curl แล้วตัดมาเฉพาะแท็กหัวข้อ) — ไม่มี error สักตัว แต่ชื่อหายไป:

<h1>สวัสดีคุณ !</h1>

ใบสั่งมีช่อง name แต่โค้ดไปหยิบช่อง nmae ซึ่งไม่มี → ได้ nil (ไม่มีค่า) → #{nil} กลายเป็นข้อความว่าง · Ruby ไม่ฟ้อง เพราะการถามหาช่องที่ไม่มีใน params เป็นเรื่องปกติ — ตรงนี้แหละที่ยาก: ถ้าเราไม่รู้คำตอบล่วงหน้า เราจะไม่รู้เลยว่าพังตรงไหน

ทางแรกที่คุ้นมือ: พิมพ์ค่าออกมาดูด้วย puts

ตอนเขียน Go คุณหาบั๊กด้วยการแทรก fmt.Println("name =", name) ไว้ตรงที่สงสัย แล้วรันใหม่
ใน Ruby ทำแบบเดียวกันได้ด้วย puts หรือ p — ลองแทรกสองบรรทัดนี้ใน action:

def hello
  @name = params[:nmae]
  puts "ชื่อที่ได้: #{@name}"   # เหมือน fmt.Println ของ Go
  p params                       # p = พิมพ์หน้าตาจริงของค่า
  @message = "สวัสดีคุณ #{@name}!"
end

รีเฟรชหน้าเว็บ · ผลไม่ได้ขึ้นในเบราว์เซอร์ แต่ขึ้นในหน้าต่างเทอร์มินัลที่รัน bin/rails server อยู่ (ผลจริง):

Started GET "/hello?name=Somchai" for ::1 at 2026-11-03 21:04:30 +0700
Processing by PagesController#hello as */*
  Parameters: {"name" => "Somchai"}
ชื่อที่ได้:
#<ActionController::Parameters {"name" => "Somchai", "controller" => "pages", "action" => "hello"} permitted: false>
  Rendering layout layouts/application.html.erb
…
Completed 200 OK in 10ms (Views: 6.4ms | ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 0.0ms)

ได้ผลจริง แต่มีปัญหาสองข้อ: ต้องเดาล่วงหน้าว่าจะพิมพ์ค่าไหน (เดาไม่ถูกก็ต้องแก้โค้ดแล้วรีเฟรชใหม่อีกรอบ) · และพอเจอบั๊กแล้วต้องตามลบ puts ทุกบรรทัด

📸 เทียบให้เห็นภาพ — ติดกล้องถ่ายรูปไว้ในครัว

puts เหมือนติดกล้องที่ถ่ายรูปอัตโนมัติไว้ไม่กี่มุมในครัว · ร้านเปิด ใบสั่งวิ่งผ่าน กล้องกดชัตเตอร์ · คุณได้รูปมาดูทีหลัง — แต่เห็นแค่มุมที่ติดกล้องไว้
ถ้าหม้อที่อยากดูไม่อยู่ในรูป ต้องไปติดกล้องเพิ่มแล้วรอใบสั่งใบใหม่

ที่ไม่เหมือนกัน: กล้องจริงไม่ทำให้อาหารช้าลง และถอดออกเมื่อไหร่ก็ได้ · แต่ puts เป็นโค้ดจริงที่อยู่ในไฟล์ — ลืมลบแล้ว push ขึ้นไป มันจะพิมพ์ทุกครั้งที่มีคนเข้าเว็บ จนเต็ม log ของ server จริง

🗣 ศัพท์ที่เพิ่งเรียนไป
print debugging
การหาบั๊กด้วยการแทรกคำสั่งพิมพ์ค่า (puts · p · fmt.Println) · ง่ายและใช้ได้ทุกภาษา แต่ต้องเดาว่าจะพิมพ์อะไร · ถาม senior ได้ว่า “ทีมนี้หาบั๊กใน Rails ด้วย debugger หรือ print debugging มากกว่ากัน”
server log
ข้อความที่ bin/rails server พิมพ์ในเทอร์มินัลทุกครั้งที่มี request เข้ามา (Started GET … · Completed 200 OK …) · ผลของ puts ใน controller จะไปโผล่ตรงนี้ ไม่ใช่บนหน้าเว็บ · พิมพ์บอก AI ได้ว่า “here is my Rails server log for this request” แล้วแปะทั้งก้อน — ได้คำตอบตรงกว่าเล่าเป็นคำพูด

debugger — กดหยุดเวลาในครัว

debugger คือคำสั่งที่บอก Ruby ว่า "พอมาถึงบรรทัดนี้ หยุดก่อน แล้วเปิดช่องให้ฉันพิมพ์ถาม"
ระหว่างหยุด คุณพิมพ์ชื่อตัวแปรตัวไหนก็ได้เพื่อดูค่า — ไม่ต้องเดาล่วงหน้า · ดูพอแล้วสั่งให้วิ่งต่อ

⏸ เทียบให้เห็นภาพ — กดหยุดเวลาในครัว แล้วเดินไปเปิดหม้อดู

ใบสั่งวิ่งเข้าครัว เชฟกำลังทำอาหาร แล้วเวลาหยุดนิ่งตรงจุดที่คุณแปะป้าย debugger ไว้ · คุณเดินไปเปิดหม้อใบไหนก็ได้ (พิมพ์ @name) · หยิบใบสั่งขึ้นมาอ่าน (พิมพ์ params) · แล้วกดให้เวลาเดินทีละก้าว (next) หรือเดินต่อตามปกติ (continue)

ที่ไม่เหมือนกัน: ในนิยายเวลาหยุดแล้วไม่มีใครรู้ตัว · แต่ในเว็บ ลูกค้ารอจริง — เบราว์เซอร์จะหมุนค้างตลอดเวลาที่คุณยังไม่สั่ง continue · และถ้าคุณพิมพ์โค้ดที่ไปเปลี่ยนค่า (เช่น @name = "x") ระหว่างหยุด มันเปลี่ยนจริง ไม่ใช่แค่ดู

เทียบกับ Go: Go ก็มีเครื่องมือแบบนี้ ชื่อ Delve (คำสั่ง dlv) แต่ไม่ได้มากับ Go ต้องติดตั้งเพิ่มเอง · ฝั่ง Rails ไม่ต้องติดตั้งอะไร เพราะ rails new ใส่บรรทัดนี้ไว้ใน Gemfile ให้แล้ว (ของจริงจากแอปที่สร้างใหม่):

group :development, :test do
  # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
  gem "debug", platforms: %i[ mri windows ], require: "debug/prelude"
  …
end
🗣 ศัพท์ที่เพิ่งเรียนไป
debugger · breakpoint
debugger คือโปรแกรมที่หยุดโค้ดกลางทางให้เราดูข้างใน · จุดที่สั่งให้หยุดเรียกว่า breakpoint · ใน Ruby แค่เขียนคำว่า debugger ลงในบรรทัดที่อยากให้หยุด · พิมพ์บอก AI ได้ว่า “I put a debugger breakpoint in my Rails controller action, here is what I see”
gem debug · rdbg
debugger มาตรฐานของ Ruby · ตัวโปรแกรมชื่อ rdbg — ตอนหยุด จอจะขึ้น prompt ว่า (rdbg) · ถาม senior ได้ว่า “ทีมนี้ใช้ rdbg ในเทอร์มินัล หรือต่อเข้า VS Code”

ลองหยุดจริง: ใส่ debugger ใน action

ลบ puts สองบรรทัดเมื่อกี้ออก แล้วใส่คำว่า debugger หนึ่งบรรทัดแทน:

class PagesController < ApplicationController
  def hello
    @name = params[:nmae]
    debugger                        # ← หยุดตรงนี้
    @message = "สวัสดีคุณ #{@name}!"
  end
end

⚠️ เปิด server ด้วย bin/rails server เท่านั้น อย่าใช้ bin/dev (ที่บท 98 บอกว่าเพื่อนบางคนใช้) · ผมลองแล้ว: ใต้ bin/dev จอขึ้นแค่ DEBUGGER: wait for debugger connection... แล้วพิมพ์อะไรไปก็ไม่ตอบ เบราว์เซอร์ค้างอย่างเดียว · สาเหตุ: bin/dev ตั้งให้ debugger รอโปรแกรมอื่นมาต่อเข้ามา แทนที่จะรับสิ่งที่คุณพิมพ์ในหน้าจอนั้น

เปิด http://localhost:3000/hello?name=Somchai ในเบราว์เซอร์ · หน้าเว็บจะหมุนค้าง ไม่ขึ้นอะไร — ไม่ใช่พัง แต่โค้ดกำลังหยุดรอคุณอยู่ · ให้ไปดูเทอร์มินัลที่รัน server จะกลายเป็นแบบนี้ (ผลจริง):

Started GET "/hello?name=Somchai" for ::1 at 2026-11-03 21:04:30 +0700
…
Processing by PagesController#hello as */*
  Parameters: {"name" => "Somchai"}
[1, 7] in ~/wongnok_blog/app/controllers/pages_controller.rb
     1| class PagesController < ApplicationController
     2|   def hello
     3|     @name = params[:nmae]
=>   4|     debugger
     5|     @message = "สวัสดีคุณ #{@name}!"
     6|   end
     7| end
=>#0	PagesController#hello at ~/wongnok_blog/app/controllers/pages_controller.rb:4
  #1	ActionController::BasicImplicitRender#send_action(method="hello", args=[]) at …
  # and 78 frames (use `bt' command for all frames)
(rdbg)

อ่านจอนี้ทีละส่วน:

ทีนี้พิมพ์ถามทีละบรรทัด (กด Enter ทุกบรรทัด) — ผลจริง:

(rdbg) params
#<ActionController::Parameters {"name" => "Somchai", "controller" => "pages", "action" => "hello"} permitted: false>
(ruby) @name
nil
(ruby) params[:name]
"Somchai"

ต่อไปจะพิมพ์ next = "ทำบรรทัดนี้ แล้วหยุดที่บรรทัดถัดไป" · จะเห็นข้อความ # command ต่อท้าย — คุณไม่ได้พิมพ์เอง debugger เขียนจาง ๆ ให้ เพื่อบอกว่ามันเข้าใจว่านี่คือคำสั่งของ debugger ไม่ใช่ชื่อตัวแปร

(rdbg) next    # command
…
=>   5|     @message = "สวัสดีคุณ #{@name}!"
…
(ruby) @message

พิมพ์ next แล้วลูกศรเลื่อนมาชี้บรรทัด 5 · ตอนนี้ถาม @message จะได้ "สวัสดีคุณ !" หรืออย่างอื่น?

ผลจริง — ได้ nil:

(ruby) @message
nil
(rdbg) next    # command
…
=>   6|   end
…
(ruby) @message
"สวัสดีคุณ !"

ลูกศรชี้บรรทัดที่ "กำลังจะทำ" ไม่ใช่บรรทัดที่ "ทำไปแล้ว" · ต้อง next อีกครั้งให้บรรทัด 5 ทำงานก่อน ถึงจะเห็นค่า · นี่คือจุดที่มือใหม่ตกใจบ่อยที่สุด — เห็น nil แล้วคิดว่าโค้ดพัง ทั้งที่แค่ยังไม่ถึง

ดูพอแล้ว พิมพ์ continue ปล่อยให้โค้ดวิ่งต่อ · หน้าเว็บจะขึ้นทันที และ log บอกว่า (ผลจริง):

(rdbg) continue    # command
  Rendering layout layouts/application.html.erb
  Rendering pages/hello.html.erb within layouts/application
…
Completed 200 OK in 17946ms (Views: 19.0ms | ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 10.2ms)

17946ms ≈ 18 วินาที — คือเวลาที่ผมหยุดพิมพ์ถามอยู่ · ตลอด 18 วินาทีนั้นเบราว์เซอร์รอค้าง · งานจริงของหน้านี้ใช้แค่ราว 10ms (ดูจาก log ตอนใช้ puts ข้างบน)

สรุปคำสั่งที่พิมพ์ได้ตอนหยุด:

พิมพ์ย่อทำอะไร
ชื่อตัวแปร / โค้ด Ruby—ดูค่าทันที เช่น @name · params[:name]
nextnทำบรรทัดนี้ แล้วหยุดที่บรรทัดถัดไป
stepsถ้าบรรทัดนี้เรียก method ให้มุดเข้าไปดูข้างใน · ใน Rails ระวังหลงเข้าไปในโค้ดของ Rails 78 ชั้นนั้น
info—ขอดูตัวแปรทุกตัวที่มองเห็นจากตรงนี้
continuecปล่อยให้วิ่งต่อตามปกติ (หน้าเว็บจะขึ้น)

ลอง info ดูหนึ่งครั้งตอนหยุดอยู่ที่บรรทัด 4 (ผลจริง · ถ้าลองหลัง next จะเห็น @message เพิ่มมาด้วย) — ตัวแปรที่ขึ้นต้นด้วย @_ เป็นของ Rails ทั้งหมด · ของเราคือ @name บรรทัดล่างสุด · %self บรรทัดแรกคือตัว controller ที่กำลังทำงานอยู่:

(rdbg) info    # command
%self = #<PagesController:0x00000000006000>
@_action_has_layout = true
@_action_name = "hello"
…
@_request = #<ActionDispatch::Request GET "http://localhost:3000/hello?name=...>
…
@_view_runtime = nil
@name = nil
🗣 ศัพท์ที่เพิ่งเรียนไป
step over (next) · step into (step)
สองแบบของการ "เดินทีละก้าว" · over = ทำบรรทัดนี้ทั้งบรรทัดแล้วหยุดบรรทัดถัดไป · into = มุดเข้าไปใน method ที่บรรทัดนี้เรียก · ถาม senior ได้ว่า “ตรงนี้ควร step into ไหม หรือข้ามไปเพราะเป็นโค้ดของ library”
frame / backtrace
"ชั้น" ของการเรียกกันที่ซ้อนอยู่ (#0 = ชั้นที่หยุดอยู่ · #1 = คนที่เรียกมา) · ทั้งกองเรียกว่า backtrace · พิมพ์บอก AI ได้ว่า “here is the backtrace, which frame is my code?” — มันจะชี้ให้ว่าชั้นไหนเป็นของเรา

debugger หยุดอยู่ และลูกศร => ชี้ที่บรรทัด @total = 10 + 5 · พิมพ์ @total จะได้อะไร?

ตัวเลือกที่ 3 ถูก — ลูกศรชี้บรรทัดที่ "กำลังจะทำ" ตรงกับที่เห็นในเฉลย predict: ลูกศรอยู่ที่บรรทัด 5 แต่ @message ยังเป็น nil

ตัวเลือกที่ 1 ผิด เพราะกลับด้าน — ต้อง next หนึ่งครั้งก่อนถึงจะได้ 15 · ตัวเลือกที่ 2 ผิด เพราะ debugger หยุดก่อนเริ่มบรรทัด ไม่ใช่หยุดกลางบรรทัด · ตัวเลือกที่ 4 ผิด เพราะการพิมพ์ชื่อตัวแปรคือสิ่งที่ debugger มีไว้ให้ทำ

จุดที่ต้องจำ: ลูกศร = บรรทัดที่ยังไม่ทำ · อยากเห็นผลของบรรทัดนั้น กด next ก่อน

ห้ามลืม: เอา debugger ออกก่อน push

แก้บั๊กเสร็จ (เปลี่ยน :nmae เป็น :name) แต่ลืมลบบรรทัด debugger แล้ว push ขึ้นเว็บจริง จะเกิดอะไร?
จำบรรทัด group :development, :test ใน Gemfile ได้ไหม — บนเว็บจริง (production) gem debug ไม่ถูกโหลด → Ruby ไม่รู้จักคำว่า debugger

⚠️ ลืม debugger ไว้ = ทุกคนที่เปิดหน้านั้นบนเว็บจริงเจอ error 500 · ผมลองเปิดแอปในโหมด production (โหมดเว็บจริง) บนเครื่องตัวเอง แล้วส่งใบสั่งเข้า /hello — คำสั่ง app.get ที่ใช้ยิง เดี๋ยวได้เรียนในหัวข้อถัดไป ตอนนี้ดูแค่ผล:

wongnok-blog(prod):001> app.get "/hello?name=Somchai"
[22caf0bb-…] Started GET "/hello?name=Somchai" for 127.0.0.1 at 2026-11-03 21:03:56 +0700
[22caf0bb-…] Processing by PagesController#hello as HTML
[22caf0bb-…]   Parameters: {"name" => "Somchai"}
[22caf0bb-…] Completed 500 Internal Server Error in 4ms (ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 0.0ms)
…
[22caf0bb-…] NameError (undefined local variable or method 'debugger' for an instance of PagesController):
[22caf0bb-…] app/controllers/pages_controller.rb:4:in 'PagesController#hello'
…
=> 500

Completed 500 = ครัวพัง ส่งจานไม่ได้ · NameError = "ไม่รู้จักชื่อนี้" · บรรทัดถัดมาชี้ว่าพังที่ไฟล์ไหน บรรทัดที่ 4 · [22caf0bb-…] หน้าบรรทัดคือเลขประจำใบสั่ง (ผมย่อให้สั้น ของคุณจะต่าง)

ในเครื่องเราหยุดรอ · บนเว็บจริงพัง — ไม่มีอะไรเตือนให้อัตโนมัติ

นิสัยที่ควรมี: ก่อน commit ค้นหาคำว่า debugger ในโฟลเดอร์ app/ ทุกครั้ง — ผลจริงก่อนและหลังลบ:

$ grep -rn debugger app/
app/controllers/pages_controller.rb:4:    debugger
# ↑ ยังเจอ: ชื่อไฟล์ : เลขบรรทัด : เนื้อหาบรรทัด → ไปลบให้หมด

$ grep -rn debugger app/
# ↑ ลบแล้ว: ไม่มีอะไรขึ้นเลย = สะอาด พร้อม commit

ลืมบรรทัด debugger ไว้ใน action แล้วเอาขึ้นเว็บจริง (deploy) (Gemfile ตามที่ rails new ให้มา) ผู้ใช้เปิดหน้านั้นจะเจออะไร?

ตัวเลือกที่ 4 ถูก — gem debug อยู่ใน group :development, :test · production เลยไม่รู้จัก debugger · ผลจริงคือ NameError และ => 500

ตัวเลือกที่ 1 ผิด เพราะ debugger ในเว็บจริงไม่ใช่คำที่ถูกข้าม แต่เป็นชื่อที่ไม่มีใครรู้จัก · ตัวเลือกที่ 2 คือสิ่งที่เกิดในเครื่องเรา (development) ไม่ใช่บนเว็บจริง · ตัวเลือกที่ 3 ผิด เพราะไม่มีอะไรห้ามไว้ — เราต้อง grep เช็คเอง

จุดที่ต้องจำ: เครื่องเรา = หยุดรอ · เว็บจริง = 500 · ก่อน commit รัน grep -rn debugger app/ ให้ว่างเปล่า

bin/rails console — ครัวทดลองหลังร้าน

debugger ต้องรอให้มีใบสั่งวิ่งเข้ามาชนก่อน · แต่บางคำถามไม่เกี่ยวกับใบสั่งเลย เช่น "ชื่อ route นี้ได้ URL อะไร" หรือ "link_to แบบนี้ได้ HTML หน้าตายังไง"
bin/rails console คือหน้าจอที่โหลดแอปทั้งตัวขึ้นมา แล้วให้คุณพิมพ์ Ruby ทีละบรรทัด กด Enter ได้ผลทันที — ไม่ต้องเปิด server ไม่ต้องเปิดเบราว์เซอร์

🍳 เทียบให้เห็นภาพ — ครัวทดลองหลังร้าน

ร้านยังไม่เปิด ไม่มีลูกค้า แต่คุณเดินเข้าครัวหลังร้านได้ · ทุกอย่างในครัวเป็นของจริงชุดเดียวกับร้าน — หัวหน้าเชฟทุกแผนก (controller) · รายการในมือพนักงานต้อนรับ (routes) · เครื่องมือจัดจาน (helper) · อยากรู้อะไรก็หยิบมาลองทีละอย่าง ไม่ต้องรอลูกค้าสั่ง

ที่ไม่เหมือนกัน: คนมักคิดว่าครัวทดลองทำอะไรก็ไม่มีผล · แต่ console ใช้ตู้เก็บแฟ้ม (database) ใบเดียวกับที่ server ในเครื่องเราใช้ — วันที่ 2 เมื่อมีข้อมูลบทความจริง ถ้าคุณสั่งลบใน console ข้อมูลก็หายจริง · อยากได้ครัวที่ลองแล้วทิ้งได้ ต้องเปิดแบบ --sandbox (หัวข้อถัดไป)

เทียบกับ Go: Go ไม่มีหน้าจอแบบนี้ · อยากลองเรียกฟังก์ชันหนึ่งตัวต้องเขียนไฟล์ main.go ใหม่ แล้ว go run · ใน Ruby คุณเคยใช้ irb มาแล้ว (บท 91) — bin/rails console ก็คือ irb ที่โหลดแอปทั้งตัวไว้ให้ก่อน

เปิดในโฟลเดอร์โปรเจกต์ (ผลจริง):

$ bin/rails console
Loading development environment (Rails 8.1.4)
wongnok-blog(dev):001>

ลองสามเรื่องที่เรียนมาในบทก่อน ๆ — ผลจริง:

wongnok-blog(dev):001> "post".pluralize
=> "posts"
wongnok-blog(dev):002> "posts".classify
=> "Post"
wongnok-blog(dev):003> app.hello_path
=> "/hello"
wongnok-blog(dev):004> helper.link_to "หน้าทักทาย", "/hello"
=> "<a href=\"/hello\">หน้าทักทาย</a>"

ยิง request จาก console ด้วย app.get — และกับดัก 403

app ยังทำได้มากกว่านั้น: app.get "/hello" = สั่งเบราว์เซอร์จำลองส่งใบสั่งเข้าแอป โดยไม่ต้องเปิด server · ถ้าคุณเคยเขียน httptest ใน Go (บท 40) นี่คือแนวคิดเดียวกัน — สร้างใบสั่งปลอมแล้วส่งเข้า handler ตรง ๆ · ลองพิมพ์ดู ผลจริง:

wongnok-blog(dev):005> app.get "/hello?name=Somchai"
[ActionDispatch::HostAuthorization::DefaultResponseApp] Blocked hosts: www.example.com
=> 403

403 = ถูกห้ามเข้า · ใบสั่งไม่ถึงพนักงานต้อนรับ (routes) ด้วยซ้ำ — ถูกด่านหน้าครัว (middleware บท 100) ชื่อ HostAuthorization ตีกลับ · Blocked hosts: www.example.com บอกเหตุผลตรง ๆ: เบราว์เซอร์จำลองแอบอ้างว่ามาจากเว็บชื่อ www.example.com แต่ตอน development Rails ยอมรับแค่ชื่ออย่าง localhost (กันการโจมตีแบบปลอมชื่อเว็บ — ตั้งใจ ไม่ใช่บั๊ก)

แก้ด้วยการบอกเบราว์เซอร์จำลองให้ใช้ชื่อ localhost — ผลจริง:

wongnok-blog(dev):006> app.host! "localhost"
=> "localhost"
wongnok-blog(dev):007> app.get "/hello?name=Somchai"
Started GET "/hello?name=Somchai" for 127.0.0.1 at 2026-11-03 21:11:19 +0700
…
Processing by PagesController#hello as HTML
  Parameters: {"name" => "Somchai"}
…
Completed 200 OK in 12ms (Views: 6.0ms | ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 0.0ms)
=> 200
wongnok-blog(dev):008> app.response.body[/<h1>.*<\/h1>/]
=> "<h1>สวัสดีคุณ Somchai!</h1>"

⚠️ console โหลดโค้ดครั้งเดียวตอนเปิด · ถ้าไปแก้ไฟล์ใน app/ ระหว่างที่ console เปิดอยู่ ให้พิมพ์ reload! ก่อนลองใหม่ (ผลจริง: ขึ้น Reloading... แล้ว => nil) · ไม่งั้นจะได้ผลของโค้ดเก่า แล้วงงว่าทำไมแก้แล้วไม่เปลี่ยน

🗣 ศัพท์ที่เพิ่งเรียนไป
REPL · Rails console
REPL ย่อมาจาก Read–Eval–Print Loop = อ่านที่พิมพ์ → รัน → โชว์ผล → วนรอบใหม่ · irb คือ REPL ของ Ruby · bin/rails console คือ REPL ที่โหลดแอปมาด้วย · ถาม senior ได้ว่า “อันนี้ลองใน rails console ก่อนได้ไหม ก่อนเขียนลงไฟล์” — คนทำ Rails ทำแบบนี้เป็นปกติ
app · helper
ตัวช่วยสองตัวที่มีเฉพาะใน console: app = เบราว์เซอร์จำลอง (app.get · app.host! · app.response) · helper = เรียก view helper · พิมพ์บอก AI ได้ว่า “in rails console, app.get returns 403 Blocked hosts: www.example.com” แล้วมันจะตอบเรื่อง host ทันที
Host Authorization
ด่านหน้าครัวที่เช็คว่าใบสั่งมาจากชื่อเว็บที่อนุญาตไหม · เห็น Blocked hosts แปลว่าติดด่านนี้ ไม่ใช่ route ผิด · ถาม senior ได้ว่า “เว็บจริงของเราตั้ง config.hosts ไว้เป็นชื่อโดเมนอะไร”

เปิด bin/rails console แล้วพิมพ์ app.get "/hello" ได้ => 403 พร้อมข้อความ Blocked hosts: www.example.com ควรทำอะไรต่อ?

ตัวเลือกที่ 2 ถูก — ข้อความบอกว่าติดด่านชื่อเว็บ (www.example.com ไม่ได้รับอนุญาต) · ตั้งเป็น localhost แล้วผลจริงได้ => 200

ตัวเลือกที่ 1 ผิด เพราะ app ไม่ต้องพึ่ง server เลย — ผมยิงได้ 200 ทั้งที่ไม่มี server เปิดอยู่ · ตัวเลือกที่ 3 ผิด เพราะถ้าไม่มี route จะได้ 404 ไม่ใช่ 403 · ตัวเลือกที่ 4 ผิด เพราะ reload! ใช้ตอนแก้โค้ด ไม่ได้เปลี่ยนชื่อเว็บของเบราว์เซอร์จำลอง

จุดที่ต้องจำ: app.get ได้ 403 + Blocked hosts → app.host! "localhost" ก่อนอย่างอื่น

--sandbox — ลองแล้วทิ้ง (จะสำคัญมากในวันที่ 2)

วันนี้แอปของเรายังไม่ได้เก็บข้อมูลอะไรลงฐานข้อมูล · แต่วันที่ 2 จะมีตารางบทความจริง และคุณจะใช้ console สร้าง แก้ ลบ บทความเยอะมาก · ถ้าอยากลองคำสั่งที่น่ากลัว (เช่น ลบทั้งตาราง) โดยไม่ให้ข้อมูลหายจริง ให้เปิดแบบนี้ (ผลจริง):

$ bin/rails console --sandbox
Loading development environment in sandbox (Rails 8.1.4)
Any modifications you make will be rolled back on exit
wongnok-blog(dev):001> 1 + 1
=> 2
wongnok-blog(dev):002> exit

ข้อความบรรทัดที่สองแปลว่า "ทุกอย่างที่คุณเปลี่ยนในฐานข้อมูล จะถูกย้อนกลับตอนออก" · พิมพ์คำสั่งได้เหมือน console ปกติทุกอย่าง ต่างกันแค่ตอน exit ทุกการเปลี่ยนแปลงข้อมูลถูกยกเลิก

✏️ เทียบให้เห็นภาพ — เขียนบนแผ่นใสที่วางทับแฟ้มในตู้

database คือตู้เก็บแฟ้มของร้าน · console ปกติคือเขียนปากกาลงแฟ้มจริง · --sandbox คือวางแผ่นใสทับแฟ้มแล้วเขียนบนแผ่นใส — มองเห็นของจริงข้างใต้ ขีดฆ่าได้ตามใจ · พอ exit ก็ยกแผ่นใสทิ้ง แฟ้มในตู้เหมือนเดิม

ที่ไม่เหมือนกัน: แผ่นใสคุมได้แค่ของในตู้เก็บแฟ้ม (database) · ถ้าคุณสั่งอย่างอื่นที่ออกไปนอกฐานข้อมูล (เช่น ส่งอีเมล หรือเขียนไฟล์) สิ่งนั้นเกิดขึ้นจริง ยกแผ่นใสทิ้งก็ไม่ย้อนกลับ

เทียบกับ Go: จำ defer tx.Rollback() ในบท 8 ได้ไหม — เปิด transaction (กลุ่มคำสั่งที่ "ยืนยันทีเดียว หรือยกเลิกทีเดียว") แล้วยกเลิกทั้งก้อน · --sandbox คือ Rails เปิด transaction ครอบไว้ให้ตั้งแต่เปิด console แล้วสั่ง rollback ให้ตอน exit · ไม่ต้องเขียนเอง

สไลด์หน้า 69 ยังพูดถึงอีกสองตัว แค่รู้ไว้ก่อน: bin/rails console -e production = เปิด console ของเว็บจริง (prompt จะเป็น (prod) สีแดง — เห็นสีแดงให้หยุดมือก่อนกด Enter) · bin/rails dbconsole = เข้าโปรแกรมพิมพ์ SQL ของฐานข้อมูลตรง ๆ (ต้องมีโปรแกรม psql ในเครื่อง — รอวันที่ 2)

🗣 ศัพท์ที่เพิ่งเรียนไป
sandbox · rollback
sandbox = โหมดลองเล่นที่ไม่มีผลถาวร · rollback = ยกเลิกการเปลี่ยนแปลงทั้งก้อน (ตรงข้ามกับ commit = ยืนยัน) · กลไกเบื้องหลังคือ transaction แบบเดียวกับ tx.Rollback() ของ Go · ถาม senior ได้ว่า “จะลองคำสั่งนี้กับข้อมูล ควรเปิด console --sandbox ก่อนไหม”
environment (development / test / production)
ชุดการตั้งค่าที่แอปใช้ · development = เครื่องเรา · production = เว็บจริงที่ลูกค้าใช้ · -e คือเลือกชุด · ถาม senior ได้ว่า “ทีมเราเข้า console ของ production ยังไง ต้องขอสิทธิ์ใคร” — ควรรู้ก่อนวันที่ต้องใช้จริง

จับคู่ปัญหากับเครื่องมือ: "อยากรู้ว่า params ตอนผู้ใช้กดปุ่มส่งฟอร์มมีค่าอะไร" ควรใช้อะไร?

ตัวเลือกที่ 1 ถูก — params คือรายละเอียดในใบสั่ง มีได้เฉพาะตอนที่มีใบสั่งวิ่งเข้ามาจริง · debugger หยุดตรงนั้นแล้วให้ถามได้ (เหมือนที่เราถามเห็น "name" => "Somchai")

ตัวเลือกที่ 2 และ 3 ผิด เพราะใน console ยังไม่มีใบสั่งเข้ามา จึงไม่มี params ให้ดู (console เหมาะกับคำถามที่ไม่ต้องรอลูกค้า เช่น ชื่อ route · ผลของ helper) · ตัวเลือกที่ 4 ผิด เพราะ params ไม่ได้ถูกเก็บในฐานข้อมูล

จุดที่ต้องจำ: ต้องมีใบสั่งวิ่งมา → debugger · ลองของได้โดยไม่ต้องมีลูกค้า → console · ลองของที่น่ากลัวกับข้อมูล → console --sandbox

🎯 ภารกิจ — หาบั๊กด้วยตัวเอง (~10 นาที)

ทำในแอป wongnok_blog ของคุณ (ถ้ายังไม่มี ให้ทำตามบท 98 ก่อน) · ทุกขั้นใช้ชื่อภาษาอังกฤษ เช่น Somchai (ชื่อภาษาไทยจะพังในขั้นที่ 6 — ดูท้ายเฉลย)

  1. เพิ่มบรรทัด get "/hello", to: "pages#hello" ใน config/routes.rb (ใต้ Rails.application.routes.draw do) · สร้างไฟล์ใหม่ app/controllers/pages_controller.rb · สร้างโฟลเดอร์ app/views/pages/ แล้วสร้างไฟล์ hello.html.erb ข้างใน · เนื้อหาตามโค้ดในหัวข้อแรกของบทนี้ (ใส่ :nmae สะกดผิดไว้ด้วย)
  2. ใส่ debugger ใต้บรรทัด @name = … · เปิด bin/rails server (ไม่ใช่ bin/dev)
  3. เปิดเบราว์เซอร์ที่ http://localhost:3000/hello?name=Somchai (เปลี่ยนเป็นชื่อภาษาอังกฤษของคุณได้) → หน้าเว็บควรหมุนค้าง
  4. ไปที่เทอร์มินัลของ server · พิมพ์ params · @name · params[:name] ทีละบรรทัด แล้ว continue
  5. แก้ :nmae เป็น :name · ลบ debugger · รัน grep -rn debugger app/ ให้ไม่มีอะไรขึ้น
  6. เปิด bin/rails console · พิมพ์ app.host! "localhost" แล้ว app.get "/hello?name=Somchai" ให้ได้ => 200

ก่อนกดเฉลย: ข้อ 4 ตอนพิมพ์ @name ได้อะไร และข้อ 6 ถ้าข้าม app.host! จะได้เลขอะไร?

สิ่งที่ควรเห็น (จากเครื่องที่ใช้ตรวจ):

  • ข้อ 3 — เบราว์เซอร์หมุนค้าง · เทอร์มินัลขึ้นโค้ด 7 บรรทัด ลูกศรชี้ debugger และ prompt (rdbg)
  • ข้อ 4 — params มีช่อง "name" · @name ได้ nil · params[:name] ได้ชื่อของคุณ · หลัง continue หน้าเว็บขึ้น และ log บอก Completed 200 OK in …ms เป็นเลขหลักพันถึงหมื่น (เพราะนับเวลาที่หยุดรอด้วย)
  • ข้อ 5 — grep ไม่พิมพ์อะไรออกมาเลย
  • ข้อ 6 — ถ้าข้าม app.host! ได้ => 403 + Blocked hosts: www.example.com · ตั้งแล้วได้ => 200

ถ้าเจอ error นี้ แปลว่า…

  • เบราว์เซอร์หมุนไม่หยุด แม้แก้โค้ดแล้ว — debugger ยังหยุดรออยู่ในเทอร์มินัลของ server · ไปพิมพ์ continue ที่นั่น
  • DEBUGGER: wait for debugger connection... แล้วพิมพ์อะไรก็ไม่ตอบ — คุณเปิดด้วย bin/dev · กด Ctrl-C ปิด แล้วเปิดใหม่ด้วย bin/rails server
  • eval error: undefined local variable or method 'app' for #<PagesController…> — คุณพิมพ์ app.get ใน (rdbg) ของ debugger แทนที่จะพิมพ์ใน console · app กับ helper มีเฉพาะใน bin/rails console (ผมพลาดแบบนี้เองตอนเตรียมบท ได้ข้อความนี้จริง)
  • URI must be ascii only ตอนพิมพ์ app.get — คุณใส่ชื่อภาษาไทยลงไปใน URL ตรง ๆ · เบราว์เซอร์จริงแปลงตัวอักษรไทยให้เอง แต่เบราว์เซอร์จำลอง app ไม่แปลง → ใช้ชื่อภาษาอังกฤษแทน (ผมลองแล้ว ได้ error นี้จริง)
  • NameError (undefined local variable or method 'debugger' …) ใน log — มีคนลืมลบ debugger ไว้ แล้วโค้ดไปรันในโหมดที่ไม่มี gem debug

ติดตรงไหน หรือผลของคุณไม่ตรงกับที่เขียนไว้ ถามกลับมาได้เลย พร้อมแปะสิ่งที่จอขึ้นมาทั้งก้อน

ถ้าเปิดสไลด์ดูประกอบ

หน้าสไลด์เขียนของจริงต้องสนใจไหม
67–68ตัวอย่างใช้ Post.all · Post.count · Post.create!ต้องมีตารางบทความในฐานข้อมูลก่อน ซึ่งเป็นเรื่องวันที่ 2 · บทนี้จึงใช้หน้า /hello แทนไม่ต้อง — วันที่ 2 จะได้ลองเอง
67ผลใน debugger มี => นำหน้า · params เป็น {…} ธรรมดาไม่มี => · params โชว์เป็น #<ActionController::Parameters {…} permitted: false>ไม่ต้อง — หน้าตาต่าง ความหมายเหมือนกัน
67debugger ดีกว่า puts เพราะ "ไม่ลืมลบ puts ทิ้งก่อน commit"debugger ก็ต้องลบ · ลืมไว้ = เว็บจริง error 500สนใจมาก
68prompt เป็น irb>wongnok-blog(dev):001>ไม่ต้อง
68app.get "/posts" ได้ => 200ได้ 403 · ต้อง app.host! "localhost" ก่อนสนใจ — พิมพ์ตามแล้วจะงง
69เคล็ดลับ: ตั้งสี prompt ให้ต่างกันระหว่าง dev กับ productionRails 8.1 ทำให้แล้ว: (dev) สีน้ำเงิน · (prod) สีแดงไม่ต้องตั้งเอง

อ้างอิง