FSIBLOG

How to Fix Incorrect HTTP Error Codes in Ruby on Rails Applications

Fix Incorrect HTTP Error Codes in Ruby on Rails Applications

Key Takeaways

The Problem

When building an OAuth API provider in Ruby on Rails 3.0 (or any Rails version), you need to return semantically correct HTTP status codes so API consumers can handle errors programmatically. Consider this common controller action:

def destroy_oauth
  @item = Item.find(params[:id])
  if !@item.nil? && @item.user_id == current_user.id
    @item.destroy
    respond_to do |format|
      format.js
      format.xml
    end
  else
    raise ActionController::RoutingError.new('Forbidden')
  end
end

The developer’s intention is to return a 403 Forbidden when the user doesn’t own the item. However, Rails always returns 404 Not Found instead.

Why This HTTP Error Codes in Ruby

The root cause is straightforward: ActionController::RoutingError is an exception class that Rails specifically maps to HTTP 404. It doesn’t matter what string you pass to it 'Forbidden', 'Unauthorized', or anything else. Rails’ exception handling middleware intercepts RoutingError and renders a 404 response every time.

This exception is designed for one purpose: signaling that a requested route doesn’t exist. It was never intended for access control or general error handling.

The Correct Approach

Rails provides built-in, clean mechanisms for returning any HTTP status code. Here are the two main options.

Option 1: head Status Code Only (No Body)

If you just need to signal an error without a response body, use head:

head :forbidden

This sends a 403 Forbidden response with an empty body clean, simple, and idiomatic.

Option 2: render with status: Status Code with a Body

For APIs, you typically want to include an error message in the response body. Use render with the status: option:

render json: { error: "You are not authorized to delete this item." }, status: :forbidden

This sends a 403 Forbidden response along with a JSON body that the API consumer can parse and display.

The Complete Fix Code

Here is the corrected version of the original controller action, rewritten to follow Rails conventions and work properly as an API endpoint:

def destroy_oauth
  @item = Item.find_by(id: params[:id])

  if @item.nil?
    respond_to do |format|
      format.json { render json: { error: "Item not found." }, status: :not_found }
      format.xml  { render xml: { error: "Item not found." }, status: :not_found }
    end
    return
  end

  if @item.user_id != current_user.id
    respond_to do |format|
      format.json { render json: { error: "You are not authorized to delete this item." }, status: :forbidden }
      format.xml  { render xml: { error: "You are not authorized to delete this item." }, status: :forbidden }
    end
    return
  end

  @item.destroy
  respond_to do |format|
    format.json { render json: { message: "Item successfully deleted." }, status: :ok }
    format.xml  { render xml: { message: "Item successfully deleted." }, status: :ok }
    format.js
  end
end

What Changed and Why

find replaced with find_byItem.find(params[:id]) raises an ActiveRecord::RecordNotFound exception (which Rails maps to 404) if the record doesn’t exist. This means the code never even reaches your nil check. find_by returns nil instead, giving you control over the response.

Separate checks for “not found” and “forbidden” – These are two different error conditions and should return two different HTTP status codes: 404 for a missing resource and 403 for an authorization failure. The original code conflated both into a single else branch.

Explicit status codes on every response – Every branch now returns a clear status: :ok (200), :not_found (404), or :forbidden (403). The API consumer always knows exactly what happened.

return after error responses – Without return, Rails would continue executing the rest of the action after rendering, which causes a DoubleRenderError. The return statements ensure the method exits immediately after sending an error response.

Common Rails HTTP Status Symbols Reference

Instead of memorizing numeric codes, Rails lets you use descriptive symbols. Here are the ones you’ll use most often in API development:

SymbolHTTP CodeTypical Use
:ok200Successful request
:created201Resource successfully created
:no_content204Success with no response body
:bad_request400Malformed or invalid request
:unauthorized401Authentication required
:forbidden403Authenticated but not authorized
:not_found404Resource doesn’t exist
:unprocessable_entity422Validation errors
:internal_server_error500Unexpected server failure

A Cleaner Pattern: rescue_from

For larger applications, scattering status code logic across every controller action gets repetitive. Rails’ rescue_from lets you centralize error handling in ApplicationController:

class ApplicationController < ActionController::Base

  rescue_from ActiveRecord::RecordNotFound, with: :record_not_found
  rescue_from Pundit::NotAuthorizedError,   with: :user_not_authorized

  private

  def record_not_found
    respond_to do |format|
      format.json { render json: { error: "Resource not found." }, status: :not_found }
      format.xml  { render xml: { error: "Resource not found." }, status: :not_found }
    end
  end

  def user_not_authorized
    respond_to do |format|
      format.json { render json: { error: "You are not authorized to perform this action." }, status: :forbidden }
      format.xml  { render xml: { error: "You are not authorized to perform this action." }, status: :forbidden }
    end
  end
end

With this in place, your controller actions become much simpler:

def destroy_oauth
  @item = Item.find(params[:id])  # Raises RecordNotFound if missing — handled automatically
  authorize @item                  # Raises NotAuthorizedError if forbidden — handled automatically

  @item.destroy
  respond_to do |format|
    format.json { render json: { message: "Item successfully deleted." }, status: :ok }
    format.xml  { render xml: { message: "Item successfully deleted." }, status: :ok }
    format.js
  end
end

The error paths are now completely invisible at the action level rescue_from catches the exceptions and sends the correct HTTP responses automatically.

Summary

The fix comes down to one rule: never use exception classes to control HTTP status codes unless that exception is specifically mapped to the status you want. ActionController::RoutingError means 404, always. For everything else, use render or head with an explicit status: option, and use Rails’ symbol shortcuts to keep your code readable.

Exit mobile version