จะเปลี่ยนเส้นทางไปยัง 404 ใน Rails ได้อย่างไร?


482

ฉันต้องการ 'ปลอม' หน้า 404 ใน Rails ใน PHP ฉันจะส่งส่วนหัวพร้อมรหัสข้อผิดพลาดเช่น:

header("HTTP/1.0 404 Not Found");

ทำอย่างไรกับ Rails

คำตอบ:


1049

อย่าแสดง 404 ด้วยตัวคุณเองโดยไม่มีเหตุผล Rails มีฟังก์ชั่นนี้ติดตั้งไว้แล้ว หากคุณต้องการแสดงหน้า 404 ให้สร้างrender_404วิธี (หรือnot_foundที่ฉันเรียกว่า) ในApplicationControllerลักษณะนี้:

def not_found
  raise ActionController::RoutingError.new('Not Found')
end

ทางรถไฟยังจัดการAbstractController::ActionNotFoundและActiveRecord::RecordNotFoundในลักษณะเดียวกัน

สิ่งนี้ทำได้ดีกว่าสองสิ่ง:

1) ใช้rescue_fromตัวจัดการRails ที่สร้างขึ้นเพื่อแสดงผลหน้า 404 และ 2) มันขัดจังหวะการเรียกใช้โค้ดของคุณเพื่อให้คุณทำสิ่งที่ดีเช่น:

  user = User.find_by_email(params[:email]) or not_found
  user.do_something!

โดยไม่ต้องเขียนคำแถลงเงื่อนไขที่น่าเกลียด

เป็นโบนัสนอกจากนี้ยังง่ายต่อการจัดการในการทดสอบ ตัวอย่างเช่นในการทดสอบการรวม rspec:

# RSpec 1

lambda {
  visit '/something/you/want/to/404'
}.should raise_error(ActionController::RoutingError)

# RSpec 2+

expect {
  get '/something/you/want/to/404'
}.to raise_error(ActionController::RoutingError)

และ minitest:

assert_raises(ActionController::RoutingError) do 
  get '/something/you/want/to/404'
end

หรืออ้างถึงข้อมูลเพิ่มเติมจากRails ที่แสดง 404 ไม่พบจากการกระทำของตัวควบคุม


3
มีเหตุผลที่จะทำมันเอง หากแอปพลิเคชันของคุณจี้เส้นทางทั้งหมดจากรูท มันออกแบบไม่ดี แต่บางครั้งก็ไม่สามารถหลีกเลี่ยงได้
canmike

7
วิธีการนี้ยังช่วยให้คุณใช้ตัวค้นหาบางอย่างของ ActiveRecord (find !, find_by _... ! ฯลฯ ) ซึ่งทั้งหมดยกข้อยกเว้น ActiveRecord :: RecordNotFound ขึ้นมาหากไม่พบระเบียนใด ๆ
gjvis

2
นี่ทำให้เกิดข้อผิดพลาดเซิร์ฟเวอร์ภายใน 500 ข้อสำหรับฉันไม่ใช่ 404 ฉันหายไปอะไร
เกล็น

3
ดูเหมือนว่าActionController::RecordNotFoundเป็นตัวเลือกที่ดีกว่า
Peter Ehrlich

4
รหัสใช้งานได้ดี แต่การทดสอบไม่ได้จนกว่าฉันจะรู้ว่าฉันกำลังใช้ RSpec 2 ซึ่งมีไวยากรณ์ที่แตกต่างกัน: expect { visit '/something/you/want/to/404' }.to raise_error(ActionController::RoutingError)/ ผ่านstackoverflow.com/a/1722839/993890
ryanttb

243

HTTP 404 Status

หากต้องการส่งคืนส่วนหัว 404 เพียงใช้:statusตัวเลือกสำหรับวิธีการแสดงผล

def action
  # here the code

  render :status => 404
end

หากคุณต้องการแสดงผลหน้า 404 มาตรฐานคุณสามารถแยกคุณลักษณะในวิธีการ

def render_404
  respond_to do |format|
    format.html { render :file => "#{Rails.root}/public/404", :layout => false, :status => :not_found }
    format.xml  { head :not_found }
    format.any  { head :not_found }
  end
end

และเรียกมันว่าเป็นการกระทำของคุณ

def action
  # here the code

  render_404
end

หากคุณต้องการให้การดำเนินการแสดงหน้าข้อผิดพลาดและหยุดเพียงใช้คำสั่ง return

def action
  render_404 and return if params[:something].blank?

  # here the code that will never be executed
end

ActiveRecord และ HTTP 404

โปรดจำไว้ว่า Rails ช่วยชีวิตข้อผิดพลาด ActiveRecord บางอย่างเช่นการActiveRecord::RecordNotFoundแสดงหน้าข้อผิดพลาด 404

หมายความว่าคุณไม่จำเป็นต้องช่วยเหลือการกระทำนี้ด้วยตัวเอง

def show
  user = User.find(params[:id])
end

User.findเพิ่มActiveRecord::RecordNotFoundเมื่อไม่มีผู้ใช้ นี่เป็นคุณสมบัติที่ทรงพลังมาก ดูรหัสต่อไปนี้

def show
  user = User.find_by_email(params[:email]) or raise("not found")
  # ...
end

คุณสามารถทำให้มันง่ายขึ้นโดยมอบหมายให้ทางรถไฟตรวจสอบ เพียงใช้รุ่นปัง

def show
  user = User.find_by_email!(params[:email])
  # ...
end

9
มีปัญหาใหญ่กับโซลูชันนี้ มันจะยังคงเรียกใช้รหัสในแม่แบบ ดังนั้นถ้าคุณมีโครงสร้างที่เรียบง่ายและเงียบสงบและมีคนป้อน ID ที่ไม่มีอยู่แม่แบบของคุณจะมองหาวัตถุที่ไม่มีอยู่
jcalvert

5
ดังที่กล่าวไว้ก่อนหน้านี้ไม่ใช่คำตอบที่ถูกต้อง ลองของสตีเวน
Pablo Marambio

เปลี่ยนคำตอบที่เลือกเพื่อสะท้อนการปฏิบัติที่ดีกว่า ขอบคุณสำหรับความคิดเห็นพวก!
Yuval Karmi

1
ฉันอัพเดตคำตอบด้วยตัวอย่างเพิ่มเติมและหมายเหตุเกี่ยวกับ ActiveRecord
Simone Carletti

1
รุ่นที่บางครั้งหยุดการเรียกใช้โค้ดดังนั้นจึงเป็นทางออกที่มีประสิทธิภาพมากขึ้น
Gui vieira

60

คำตอบที่เลือกใหม่ที่ส่งมาโดย Steven Soroka นั้นใกล้ แต่ไม่สมบูรณ์ การทดสอบซ่อนความจริงที่ว่านี่ไม่ใช่การคืนค่า 404 จริง - เป็นการคืนสถานะ 200 - "สำเร็จ" คำตอบเดิมใกล้เข้ามา แต่พยายามแสดงเลย์เอาต์ราวกับว่าไม่มีความผิดพลาดเกิดขึ้น สิ่งนี้จะแก้ไขทุกอย่าง:

render :text => 'Not Found', :status => '404'

นี่คือชุดทดสอบทั่วไปของฉันสำหรับสิ่งที่ฉันคาดว่าจะได้คืน 404 โดยใช้เครื่องมือจับคู่ RSpec และ Shoulda:

describe "user view" do
  before do
    get :show, :id => 'nonsense'
  end

  it { should_not assign_to :user }

  it { should respond_with :not_found }
  it { should respond_with_content_type :html }

  it { should_not render_template :show }
  it { should_not render_with_layout }

  it { should_not set_the_flash }
end

ความหวาดระแวงที่ดีต่อสุขภาพนี้ทำให้ฉันสามารถมองเห็นความไม่ตรงกันของประเภทเนื้อหาเมื่อทุกอย่างดูเป็นเรื่องเหลวไหล :) ฉันตรวจสอบองค์ประกอบเหล่านี้ทั้งหมด: ตัวแปรที่กำหนดรหัสตอบกลับประเภทเนื้อหาตอบสนองเทมเพลตเรนเดอร์

ฉันจะข้ามการตรวจสอบประเภทเนื้อหาในแอปพลิเคชันที่เป็น html อย่างเคร่งครัด ... บางครั้ง ท้ายที่สุด "ผู้สงสัยตรวจสอบลิ้นชักทั้งหมด" :)

http://dilbert.com/strips/comic/1998-01-20/

FYI: ฉันไม่แนะนำให้ทดสอบสิ่งที่เกิดขึ้นในคอนโทรลเลอร์เช่น "should_raise" สิ่งที่คุณสนใจคือผลลัพธ์ การทดสอบของฉันด้านบนอนุญาตให้ฉันลองใช้โซลูชันที่หลากหลายและการทดสอบยังคงเหมือนเดิมไม่ว่าจะเป็นข้อยกเว้นการเพิ่มการเรนเดอร์พิเศษ ฯลฯ


3
ชอบคำตอบนี้โดยเฉพาะอย่างยิ่งเกี่ยวกับการทดสอบผลลัพธ์และไม่ใช่วิธีการที่เรียกว่าในคอนโทรลเลอร์…
xentek

ทางรถไฟมีในตัว 404 render :text => 'Not Found', :status => :not_foundสถานะ:
Lasse Bunk

1
@ JaimeBellmyer - ฉันแน่ใจว่ามันจะไม่ส่งคืน 200 เมื่อคุณอยู่ในสภาพแวดล้อมที่ปรับใช้ (เช่นการจัดเตรียม / การผลิต) ฉันทำสิ่งนี้ในหลายแอปพลิเคชันและทำงานตามที่อธิบายไว้ในโซลูชันที่ยอมรับ บางทีสิ่งที่คุณอ้างถึงคือมันส่งคืน 200 เมื่อแสดงผลหน้าจอดีบักในการพัฒนาซึ่งคุณอาจมีconfig.consider_all_requests_localพารามิเตอร์ที่ตั้งค่าเป็นจริงในenvironments/development.rbไฟล์ของคุณ หากคุณเพิ่มข้อผิดพลาดตามที่อธิบายไว้ในการแก้ปัญหาได้รับการยอมรับในการแสดงละคร / การผลิตแน่นอนคุณจะได้รับ 404 ไม่ได้เป็น 200
Javid Jamae

18

คุณสามารถใช้ไฟล์แสดงผล:

render file: "#{Rails.root}/public/404.html", layout: false, status: 404

คุณสามารถเลือกที่จะใช้เลย์เอาต์หรือไม่

ตัวเลือกอื่นคือการใช้ข้อยกเว้นเพื่อควบคุม:

raise ActiveRecord::RecordNotFound, "Record not found."

13

คำตอบที่เลือกไม่ทำงานใน Rails 3.1+ เนื่องจากตัวจัดการข้อผิดพลาดถูกย้ายไปที่มิดเดิลแวร์ (ดูปัญหา GitHub )

นี่คือวิธีการแก้ปัญหาที่ฉันพบซึ่งฉันค่อนข้างมีความสุข

ในApplicationController:

  unless Rails.application.config.consider_all_requests_local
    rescue_from Exception, with: :handle_exception
  end

  def not_found
    raise ActionController::RoutingError.new('Not Found')
  end

  def handle_exception(exception=nil)
    if exception
      logger = Logger.new(STDOUT)
      logger.debug "Exception Message: #{exception.message} \n"
      logger.debug "Exception Class: #{exception.class} \n"
      logger.debug "Exception Backtrace: \n"
      logger.debug exception.backtrace.join("\n")
      if [ActionController::RoutingError, ActionController::UnknownController, ActionController::UnknownAction].include?(exception.class)
        return render_404
      else
        return render_500
      end
    end
  end

  def render_404
    respond_to do |format|
      format.html { render template: 'errors/not_found', layout: 'layouts/application', status: 404 }
      format.all { render nothing: true, status: 404 }
    end
  end

  def render_500
    respond_to do |format|
      format.html { render template: 'errors/internal_server_error', layout: 'layouts/application', status: 500 }
      format.all { render nothing: true, status: 500}
    end
  end

และในapplication.rb:

config.after_initialize do |app|
  app.routes.append{ match '*a', :to => 'application#not_found' } unless config.consider_all_requests_local
end

และในแหล่งข้อมูลของฉัน (แสดง, แก้ไข, อัพเดท, ลบ):

@resource = Resource.find(params[:id]) or not_found

สิ่งนี้อาจได้รับการปรับปรุงให้ดีขึ้น แต่อย่างน้อยฉันก็มีมุมมองที่แตกต่างกันสำหรับ not_found และ internal_error โดยไม่แทนที่ฟังก์ชั่น Core Rails


3
นี่เป็นทางออกที่ดีมาก อย่างไรก็ตามคุณไม่ต้องการชิ้น|| not_foundส่วนเพียงแค่โทรfind!(สังเกตุเสียงดังปัง) และมันจะโยน ActiveRecord :: RecordNotFound เมื่อทรัพยากรไม่สามารถเรียกคืนได้ นอกจากนี้ให้เพิ่ม ActiveRecord :: RecordNotFound ไปยังอาร์เรย์ในเงื่อนไข if
Marek Příhoda

1
ฉันจะช่วยเหลือStandardErrorและไม่Exceptionเพียงแค่ในกรณี ที่จริงแล้วฉันจะออกจากหน้าคงที่ 500 มาตรฐานและไม่ได้ใช้กำหนดเองrender_500เลยหมายความว่าฉันจะมีrescue_fromข้อผิดพลาดที่เกี่ยวข้องกับ 404 อย่างชัดเจน
Dr.Strangelove

7

สิ่งเหล่านี้จะช่วยคุณ ...

Application Controller

class ApplicationController < ActionController::Base
  protect_from_forgery
  unless Rails.application.config.consider_all_requests_local             
    rescue_from ActionController::RoutingError, ActionController::UnknownController, ::AbstractController::ActionNotFound, ActiveRecord::RecordNotFound, with: lambda { |exception| render_error 404, exception }
  end

  private
    def render_error(status, exception)
      Rails.logger.error status.to_s + " " + exception.message.to_s
      Rails.logger.error exception.backtrace.join("\n") 
      respond_to do |format|
        format.html { render template: "errors/error_#{status}",status: status }
        format.all { render nothing: true, status: status }
      end
    end
end

ตัวควบคุมข้อผิดพลาด

class ErrorsController < ApplicationController
  def error_404
    @not_found_path = params[:not_found]
  end
end

มุมมอง / ข้อผิดพลาด / error_404.html.haml

.site
  .services-page 
    .error-template
      %h1
        Oops!
      %h2
        404 Not Found
      .error-details
        Sorry, an error has occured, Requested page not found!
        You tried to access '#{@not_found_path}', which is not a valid page.
      .error-actions
        %a.button_simple_orange.btn.btn-primary.btn-lg{href: root_path}
          %span.glyphicon.glyphicon-home
          Take Me Home

3
<%= render file: 'public/404', status: 404, formats: [:html] %>

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


1

ฉันต้องการที่จะโยน 'ปกติ' 404 สำหรับผู้ใช้ที่ลงชื่อเข้าใช้ที่ไม่ใช่ผู้ดูแลระบบดังนั้นฉันจึงเขียนสิ่งนี้ใน Rails 5:

class AdminController < ApplicationController
  before_action :blackhole_admin

  private

  def blackhole_admin
    return if current_user.admin?

    raise ActionController::RoutingError, 'Not Found'
  rescue ActionController::RoutingError
    render file: "#{Rails.root}/public/404", layout: false, status: :not_found
  end
end


0

เพื่อทดสอบการจัดการข้อผิดพลาดคุณสามารถทำสิ่งนี้:

feature ErrorHandling do
  before do
    Rails.application.config.consider_all_requests_local = false
    Rails.application.config.action_dispatch.show_exceptions = true
  end

  scenario 'renders not_found template' do
    visit '/blah'
    expect(page).to have_content "The page you were looking for doesn't exist."
  end
end

0

หากคุณต้องการจัดการ 404s ที่แตกต่างกันในรูปแบบที่แตกต่างกันให้ลองจับมันในคอนโทรลเลอร์ของคุณ สิ่งนี้จะช่วยให้คุณทำสิ่งต่าง ๆ เช่นการติดตามจำนวน 404s ที่สร้างขึ้นโดยกลุ่มผู้ใช้ที่แตกต่างกันมีการสนับสนุนการโต้ตอบกับผู้ใช้เพื่อค้นหาสิ่งที่ผิดพลาด / ส่วนใดของประสบการณ์ผู้ใช้ที่อาจต้องปรับแต่ง

ฉันได้วางตรรกะพื้นฐานไว้ที่นี่ใน ApplicationController แล้ว แต่ยังสามารถวางในคอนโทรลเลอร์ที่เฉพาะเจาะจงมากขึ้นเพื่อให้มีตรรกะพิเศษเฉพาะสำหรับคอนโทรลเลอร์หนึ่งตัวเท่านั้น

เหตุผลที่ฉันใช้ if กับ ENV ['RESCUE_404'] คือเพื่อให้ฉันสามารถทดสอบการเพิ่ม AR :: RecordNotFound ในการแยก ในการทดสอบฉันสามารถตั้งค่า ENV var ให้เป็นเท็จและ rescue_from ของฉันจะไม่ยิง วิธีนี้ฉันสามารถทดสอบการเพิ่มที่แยกจากตรรกะ 404 แบบมีเงื่อนไข

class ApplicationController < ActionController::Base

  rescue_from ActiveRecord::RecordNotFound, with: :conditional_404_redirect if ENV['RESCUE_404']

private

  def conditional_404_redirect
    track_404(@current_user)
    if @current_user.present?
      redirect_to_user_home          
    else
      redirect_to_front
    end
  end

end
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.