Bagaimana cara mengarahkan ke 404 di Rails?

482

Saya ingin 'memalsukan' halaman 404 di Rails. Dalam PHP, saya hanya akan mengirim header dengan kode kesalahan sebagai berikut:

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

Bagaimana itu dilakukan dengan Rails?

Yuval Karmi
sumber

Jawaban:

1049

Jangan membuat 404 diri Anda sendiri, tidak ada alasan untuk melakukannya; Rails sudah memiliki fungsi ini. Jika Anda ingin menampilkan halaman 404, buat render_404metode (atau not_foundseperti yang saya sebutkan) ApplicationControllerseperti ini:

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

Rel juga menangani AbstractController::ActionNotFound, dan ActiveRecord::RecordNotFounddengan cara yang sama.

Ini melakukan dua hal lebih baik:

1) Ini menggunakan rescue_fromhandler bawaan Rails ' untuk membuat halaman 404, dan 2) itu mengganggu eksekusi kode Anda, membiarkan Anda melakukan hal-hal baik seperti:

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

tanpa harus menulis pernyataan kondisional yang jelek.

Sebagai bonus, ini juga sangat mudah untuk ditangani dalam tes. Misalnya, dalam tes integrasi 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)

Dan terkecil:

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

ATAU rujuk info lebih lanjut dari Rails render 404 yang tidak ditemukan dari aksi pengontrol

Steven Soroka
sumber
3
Ada alasan untuk melakukannya sendiri. Jika aplikasi Anda membajak semua rute dari root. Desainnya buruk, tetapi terkadang tidak dapat dihindari.
ablemike
7
Pendekatan ini juga memungkinkan Anda menggunakan bang finders ActiveRecord (find !, find_by _...!, Dll.), Yang semuanya meningkatkan pengecualian ActiveRecord :: RecordNotFound jika tidak ada catatan yang ditemukan (memicu handler rescue_from).
gjvis
2
Ini menimbulkan 500 Server Internal Kesalahan bagi saya, bukan 404. Apa yang saya lewatkan?
Glenn
3
Sepertinya ActionController::RecordNotFoundini pilihan yang lebih baik?
Peter Ehrlich
4
Kode berfungsi dengan baik tetapi pengujian tidak sampai saya menyadari saya menggunakan RSpec 2 yang memiliki sintaks berbeda: expect { visit '/something/you/want/to/404' }.to raise_error(ActionController::RoutingError)/ via stackoverflow.com/a/1722839/993890
ryanttb
243

Status HTTP 404

Untuk mengembalikan header 404, cukup gunakan :statusopsi untuk metode render.

def action
  # here the code

  render :status => 404
end

Jika Anda ingin merender halaman 404 standar, Anda dapat mengekstrak fitur dalam suatu metode.

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

dan menyebutnya dalam tindakan Anda

def action
  # here the code

  render_404
end

Jika Anda ingin tindakan membuat halaman kesalahan dan berhenti, cukup gunakan pernyataan kembali.

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

  # here the code that will never be executed
end

ActiveRecord dan HTTP 404

Juga ingat bahwa Rails menyelamatkan beberapa kesalahan ActiveRecord, seperti ActiveRecord::RecordNotFoundmenampilkan halaman kesalahan 404.

Itu berarti Anda tidak perlu menyelamatkan tindakan ini sendiri

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

User.findmemunculkan ActiveRecord::RecordNotFoundketika pengguna tidak ada. Ini adalah fitur yang sangat kuat. Lihatlah kode berikut

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

Anda dapat menyederhanakannya dengan mendelegasikan ke Rails cek. Cukup gunakan versi bang.

def show
  user = User.find_by_email!(params[:email])
  # ...
end
Simone Carletti
sumber
9
Ada masalah besar dengan solusi ini; masih akan menjalankan kode di templat. Jadi, jika Anda memiliki struktur yang sederhana dan tenang dan seseorang memasukkan ID yang tidak ada, template Anda akan mencari objek yang tidak ada.
jcalvert
5
Seperti disebutkan sebelumnya, ini bukan jawaban yang benar. Coba Steven.
Pablo Marambio
Mengubah jawaban yang dipilih untuk mencerminkan praktik yang lebih baik. Terima kasih atas komentarnya, teman-teman!
Yuval Karmi
1
Saya memperbarui jawabannya dengan lebih banyak contoh dan catatan tentang ActiveRecord.
Simone Carletti
1
Versi bang TIDAK menghentikan eksekusi kode, jadi ini solusi IMHO yang lebih efektif.
Gui vieira
60

Jawaban yang baru dipilih yang dikirimkan oleh Steven Soroka sudah dekat, tetapi tidak lengkap. Tes itu sendiri menyembunyikan fakta bahwa ini tidak mengembalikan 404 yang benar - itu mengembalikan status 200 - "sukses". Jawaban aslinya lebih dekat, tetapi berusaha membuat tata letak seolah-olah tidak ada kegagalan yang terjadi. Ini memperbaiki semuanya:

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

Berikut ini adalah serangkaian tes khas saya untuk sesuatu yang saya harapkan akan kembali 404, menggunakan pencocokan RSpec dan 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

Paranoia yang sehat ini memungkinkan saya untuk menemukan ketidakcocokan tipe-konten ketika segala sesuatu tampak sangat bagus :) Saya memeriksa semua elemen ini: variabel yang ditugaskan, kode respons, tipe konten respons, template yang diberikan, tata letak yang diberikan, pesan flash.

Saya akan melewatkan pemeriksaan jenis konten pada aplikasi yang benar-benar html ... terkadang. Lagi pula, "skeptis memeriksa SEMUA laci" :)

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

FYI: Saya tidak merekomendasikan pengujian untuk hal-hal yang terjadi di controller, yaitu "should_raise". Yang Anda pedulikan adalah hasilnya. Tes saya di atas memungkinkan saya untuk mencoba berbagai solusi, dan tes tetap sama apakah solusi meningkatkan pengecualian, render khusus, dll.

Jaime Bellmyer
sumber
3
sangat suka jawaban ini, terutama yang berkaitan dengan pengujian output dan bukan metode yang disebut di controller ...
xentek
Rails telah built-in 404 Status: render :text => 'Not Found', :status => :not_found.
Lasse Bunk
1
@JaimeBellmyer - Saya yakin itu tidak mengembalikan 200 ketika Anda berada di lingkungan (yaitu staging / prod). Saya melakukan ini di beberapa aplikasi dan berfungsi seperti yang dijelaskan dalam solusi yang diterima. Mungkin yang Anda maksudkan adalah bahwa ia mengembalikan 200 ketika membuat layar debug sedang dikembangkan di mana Anda mungkin memiliki config.consider_all_requests_localparameter yang disetel ke true dalam environments/development.rbfile Anda . Jika Anda memunculkan kesalahan, seperti dijelaskan dalam solusi yang diterima, dalam pementasan / produksi, Anda pasti akan mendapatkan 404, bukan 200.
Javid Jamae
18

Anda juga dapat menggunakan file render:

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

Di mana Anda dapat memilih untuk menggunakan tata letak atau tidak.

Pilihan lain adalah dengan menggunakan Pengecualian untuk mengendalikannya:

raise ActiveRecord::RecordNotFound, "Record not found."
Paulo Fidalgo
sumber
13

Jawaban yang dipilih tidak berfungsi di Rails 3.1+ karena penangan kesalahannya dipindahkan ke middleware (lihat masalah github ).

Inilah solusi yang saya temukan yang cukup saya sukai.

Dalam 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

dan di application.rb:

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

Dan di sumber saya (tampilkan, edit, perbarui, hapus):

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

Ini tentu saja dapat ditingkatkan, tetapi setidaknya, saya memiliki pandangan yang berbeda untuk not_found dan internal_error tanpa mengesampingkan fungsi inti Rails.

Augustin Riedinger
sumber
3
ini adalah solusi yang sangat bagus; Namun, Anda tidak perlu || not_foundbagian, panggil saja find!(perhatikan bang) dan itu akan membuang ActiveRecord :: RecordNotFound ketika sumber daya tidak dapat diambil. Juga, tambahkan ActiveRecord :: RecordNotFound ke array dalam kondisi if.
Marek Příhoda
1
Saya akan menyelamatkan StandardErrordan tidak Exception, untuk berjaga-jaga. Sebenarnya saya akan meninggalkan halaman statis 500 standar dan tidak menggunakan kustom render_500sama sekali, artinya saya akan secara eksplisit rescue_from
mengatur
7

ini akan membantu Anda ...

Pengontrol Aplikasi

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

Pengontrol kesalahan

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

views / errors / 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
Caner Çakmak
sumber
3
<%= render file: 'public/404', status: 404, formats: [:html] %>

cukup tambahkan ini ke halaman yang ingin Anda render ke halaman kesalahan 404 dan Anda selesai.

Ahmed Reza
sumber
1

Saya ingin melemparkan 'normal' 404 untuk setiap pengguna yang masuk yang bukan admin, jadi saya akhirnya menulis sesuatu seperti ini di 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
dinding kosong
sumber
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
Arkadiusz Mazur
sumber
0

Untuk menguji penanganan kesalahan, Anda dapat melakukan sesuatu seperti ini:

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
Marek Příhoda
sumber
0

Jika Anda ingin menangani 404 yang berbeda dengan cara yang berbeda, pertimbangkan untuk menangkapnya di pengontrol Anda. Ini akan memungkinkan Anda untuk melakukan hal-hal seperti melacak jumlah 404 yang dihasilkan oleh kelompok pengguna yang berbeda, memiliki dukungan berinteraksi dengan pengguna untuk mencari tahu apa yang salah / bagian mana dari pengalaman pengguna yang mungkin perlu diubah, lakukan pengujian A / B, dll.

Saya di sini telah menempatkan logika dasar dalam ApplicationController, tetapi juga dapat ditempatkan di pengontrol yang lebih spesifik, untuk memiliki logika khusus hanya untuk satu pengontrol.

Alasan saya menggunakan if dengan ENV ['RESCUE_404'], adalah agar saya dapat menguji peningkatan AR :: RecordNotFound secara terpisah. Dalam tes, saya dapat mengatur ENV var ini menjadi false, dan rescue_from saya tidak akan menyala. Dengan cara ini saya dapat menguji peningkatan terpisah dari logika 404 bersyarat.

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
Houen
sumber