Làm cách nào để chuyển hướng đến 404 trong Rails?


482

Tôi muốn 'giả' một trang 404 trong Rails. Trong PHP, tôi sẽ chỉ gửi một tiêu đề với mã lỗi như sau:

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

Làm thế nào được thực hiện với Rails?

Câu trả lời:


1049

Đừng tự kết xuất 404, không có lý do gì để; Rails có chức năng này được tích hợp sẵn. Nếu bạn muốn hiển thị một trang 404, tạo ra một render_404phương pháp (hoặc not_foundnhư tôi gọi nó) trong ApplicationControllernhư thế này:

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

Rails cũng xử lý AbstractController::ActionNotFound, và ActiveRecord::RecordNotFoundcách tương tự.

Điều này làm hai điều tốt hơn:

1) Nó sử dụng Rails' xây dựng trong rescue_fromxử lý để làm cho trang 404, và 2) nó làm gián đoạn việc thực hiện mã của bạn, cho phép bạn làm những điều tốt đẹp như:

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

mà không cần phải viết báo cáo có điều kiện xấu xí.

Như một phần thưởng, nó cũng siêu dễ dàng để xử lý trong các bài kiểm tra. Ví dụ: trong thử nghiệm tích hợp 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)

Và nhỏ nhất:

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

HOẶC tham khảo thêm thông tin từ Rails render 404 không tìm thấy từ hành động của bộ điều khiển


3
Có một lý do để tự làm điều đó. Nếu ứng dụng của bạn chiếm quyền điều khiển tất cả các tuyến đường từ thư mục gốc. Đó là thiết kế xấu, nhưng đôi khi không thể tránh khỏi.
ablemike

7
Cách tiếp cận này cũng cho phép bạn sử dụng các công cụ tìm bang ActiveRecord (find!, Find_by _...!, V.v.), tất cả đều đưa ra một ngoại lệ ActiveRecord :: RecordNotFound nếu không tìm thấy bản ghi (kích hoạt trình xử lý cứu_from).
gjvis

2
Điều này đặt ra một nội Server Error 500 đối với tôi, không phải là một 404. Tôi đang thiếu gì?
Glenn

3
Có vẻ như ActionController::RecordNotFoundlà lựa chọn tốt hơn?
Peter Ehrlich

4
Mã này làm việc rất lớn nhưng các thử nghiệm đã không cho đến khi tôi nhận ra tôi đã sử dụng RSpec 2 trong đó có cú pháp khác nhau: expect { visit '/something/you/want/to/404' }.to raise_error(ActionController::RoutingError)/ qua stackoverflow.com/a/1722839/993890
ryanttb

243

HTTP 404 Status

Để trả về một tiêu đề 404, chỉ cần sử dụng :statustùy chọn cho phương thức kết xuất.

def action
  # here the code

  render :status => 404
end

Nếu bạn muốn kết xuất trang 404 tiêu chuẩn, bạn có thể trích xuất tính năng này trong một phương thức.

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

và gọi nó trong hành động của bạn

def action
  # here the code

  render_404
end

Nếu bạn muốn hành động để làm cho trang báo lỗi và dừng lại, chỉ cần sử dụng lệnh return.

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

  # here the code that will never be executed
end

ActiveRecord và HTTP 404

Cũng nhớ rằng Rails cứu một số lỗi ActiveRecord, chẳng hạn như ActiveRecord::RecordNotFoundhiển thị trang lỗi 404.

Điều đó có nghĩa là bạn không cần phải tự giải cứu hành động này

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

User.findtăng ActiveRecord::RecordNotFoundkhi người dùng không tồn tại. Đây là một tính năng rất mạnh mẽ. Nhìn vào đoạn mã sau

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

Bạn có thể đơn giản hóa nó bằng cách ủy quyền cho Rails kiểm tra. Đơn giản chỉ cần sử dụng phiên bản bang.

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

9
Có một vấn đề lớn với giải pháp này; nó vẫn sẽ chạy mã trong mẫu. Vì vậy, nếu bạn có cấu trúc đơn giản, yên tĩnh và ai đó nhập ID không tồn tại, mẫu của bạn sẽ tìm kiếm đối tượng không tồn tại.
jcalvert

5
Như đã đề cập trước đây, đây không phải là câu trả lời chính xác. Hãy thử Steven.
Pablo Marambio

Thay đổi câu trả lời được chọn để phản ánh thực tiễn tốt hơn. Cảm ơn vì lời bình luận các chàng trai!
Yuval Karmi

1
Tôi đã cập nhật câu trả lời với nhiều ví dụ và ghi chú về ActiveRecord.
Simone Carletti

1
Phiên bản bang DOES dừng thực thi mã, vì vậy đó là giải pháp IMHO hiệu quả hơn.
Gui vieira

60

Câu trả lời mới được chọn bởi Steven Soroka rất gần, nhưng không đầy đủ. Bản thân bài kiểm tra đã che giấu thực tế rằng điều này không trả về 404 thực sự - nó trả về trạng thái 200 - "thành công". Câu trả lời ban đầu gần hơn, nhưng đã cố gắng hiển thị bố cục như thể không có lỗi xảy ra. Điều này sửa mọi thứ:

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

Đây là một bộ thử nghiệm điển hình của tôi cho một thứ tôi mong đợi sẽ trả về 404, sử dụng bộ so khớp RSpec và 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

Sự hoang tưởng lành mạnh này cho phép tôi phát hiện ra sự không phù hợp về kiểu nội dung khi mọi thứ khác trông có vẻ như vậy :) Tôi kiểm tra tất cả các yếu tố này: biến được gán, mã phản hồi, loại nội dung phản hồi, mẫu được hiển thị, bố cục được hiển thị, thông báo flash.

Đôi khi tôi sẽ bỏ qua việc kiểm tra loại nội dung trên các ứng dụng hoàn toàn là html ... Rốt cuộc, "một người hoài nghi kiểm tra TẤT CẢ các ngăn kéo" :)

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

FYI: Tôi không khuyên bạn nên thử nghiệm cho những thứ đang xảy ra trong bộ điều khiển, tức là "Should_raise". Điều bạn quan tâm là đầu ra. Các thử nghiệm của tôi ở trên cho phép tôi thử các giải pháp khác nhau và các thử nghiệm vẫn giữ nguyên cho dù giải pháp đó có đưa ra một ngoại lệ, kết xuất đặc biệt, v.v.


3
thực sự thích câu trả lời này, đặc biệt là liên quan đến việc kiểm tra đầu ra và không phải là các phương thức được gọi trong bộ điều khiển
BẠCH

Rails có trạng thái 404 tích hợp : render :text => 'Not Found', :status => :not_found.
Lasse Bunk

1
@JaimeBellmyer - Tôi chắc chắn rằng nó sẽ không trả về 200 khi bạn ở trong môi trường được triển khai (tức là dàn dựng / sản phẩm). Tôi làm điều này trong một số ứng dụng và nó hoạt động như được mô tả trong giải pháp được chấp nhận. Có lẽ điều bạn đang đề cập đến là nó trả về 200 khi nó hiển thị màn hình gỡ lỗi trong quá trình phát triển nơi bạn có thể có config.consider_all_requests_localtham số được đặt thành đúng trong environments/development.rbtệp của mình . Nếu bạn nêu ra một lỗi, như được mô tả trong giải pháp được chấp nhận, trong việc dàn dựng / sản xuất, bạn chắc chắn sẽ nhận được 404, không phải 200.
Javid Jamae

18

Bạn cũng có thể sử dụng tệp kết xuất:

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

Nơi bạn có thể chọn để sử dụng bố trí hay không.

Một lựa chọn khác là sử dụng các ngoại lệ để kiểm soát nó:

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

13

Câu trả lời được chọn không hoạt động trong Rails 3.1+ vì trình xử lý lỗi đã được chuyển sang phần mềm trung gian (xem vấn đề github ).

Đây là giải pháp tôi thấy mà tôi khá hài lòng.

Trong 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

và trong application.rb:

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

Và trong tài nguyên của tôi (hiển thị, chỉnh sửa, cập nhật, xóa):

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

Điều này chắc chắn có thể được cải thiện, nhưng ít nhất, tôi có các chế độ xem khác nhau cho not_found và Internal_error mà không ghi đè các hàm Rails cốt lõi.


3
đây là một giải pháp rất hay tuy nhiên, bạn không cần || not_foundphần đó, chỉ cần gọi find!(chú ý tiếng nổ) và nó sẽ ném ActiveRecord :: RecordNotFound khi tài nguyên không thể được lấy. Ngoài ra, thêm ActiveRecord :: RecordNotFound vào mảng trong điều kiện if.
Marek Příhoda

1
Tôi sẽ giải cứu StandardErrorvà không Exception, chỉ trong trường hợp. Trên thực tế, tôi sẽ rời khỏi trang tĩnh 500 tiêu chuẩn và hoàn toàn không sử dụng tùy chỉnh render_500, nghĩa là tôi rõ ràng sẽ có một rescue_fromloạt lỗi liên quan đến 404
Dr.Strangelove

7

những thứ này sẽ giúp bạn ...

Bộ điều khiển ứng dụng

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

Bộ điều khiển lỗi

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

lượt xem / lỗi / 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] %>

chỉ cần thêm phần này vào trang bạn muốn kết xuất vào trang lỗi 404 và bạn đã hoàn thành.


1

Tôi muốn ném 404 'bình thường' cho bất kỳ người dùng nào đã đăng nhập không phải là quản trị viên, vì vậy cuối cùng tôi đã viết một cái gì đó như thế này trong 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

1
routes.rb
  get '*unmatched_route', to: 'main#not_found'

main_controller.rb
  def not_found
    render :file => "#{Rails.root}/public/404.html", :status => 404, :layout => false
  end

0

Để kiểm tra xử lý lỗi, bạn có thể làm một cái gì đó như thế này:

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

Nếu bạn muốn xử lý các 404 khác nhau theo các cách khác nhau, hãy xem xét việc bắt chúng trong bộ điều khiển của bạn. Điều này sẽ cho phép bạn thực hiện những việc như theo dõi số 404 do các nhóm người dùng khác nhau tạo ra, có hỗ trợ tương tác với người dùng để tìm ra lỗi sai / phần nào của trải nghiệm người dùng có thể cần điều chỉnh, thực hiện kiểm tra A / B, v.v.

Ở đây tôi đã đặt logic cơ sở trong ApplicationContoder, nhưng nó cũng có thể được đặt trong các bộ điều khiển cụ thể hơn, để có logic đặc biệt chỉ cho một bộ điều khiển.

Lý do tôi đang sử dụng if với ENV ['RESCUE_404'], vì vậy tôi có thể kiểm tra việc tăng AR :: RecordNotFound trong sự cô lập. Trong các thử nghiệm, tôi có thể đặt var ENV này thành false và cứu_from của tôi sẽ không kích hoạt. Bằng cách này, tôi có thể kiểm tra việc tăng tách biệt với logic 404 có điều kiện.

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
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.