Class: RuboCop::Cop::DevDoc::Style::AvoidHeadResponse

Inherits:
Base
  • Object
show all
Defined in:
lib/rubocop/cop/dev_doc/style/avoid_head_response.rb

Overview

Avoid head() with error or no-content status codes in controllers.

Rationale

Using head() for error responses returns an empty body with no useful information for the client. Error handling should be delegated to Rails exceptions (e.g. ActiveRecord::RecordNotFound) or model validations instead, which give the client more context.

head :no_content (and :reset_content) is flagged for a different reason: in a backend-driven JSON UI a bodyless "success" is ambiguous — the client needs a response body (even an empty action spec) to know what to do. A production head :no_content escape hatch surfaced as an error snackbar in front of a customer before this was flagged.

Success statuses that normally carry a body (:ok, :accepted) are legitimate uses of head() and are not flagged.

The set of flagged statuses is configurable via FlaggedStatuses:.

❌ Manually returns 404 with no body
def show
@user = User.find_by(id: params[:id])
head(:not_found) unless @user
end

✔️ Let Rails raise RecordNotFound — it renders the standard 404
def show
@user = User.find(params[:id])
end

❌ Bodyless success — a JSON-driven client learns nothing
def destroy
@resource.destroy!
head :no_content
end

✔️ Tell the client what to do, even when that is "nothing"
def destroy
@resource.destroy!
render json: {}
end

Examples:

# bad
head(:not_found)

# bad
head(:unprocessable_entity)

# bad (bodyless success — ambiguous to a JSON-driven client)
head(:no_content)

# good (success status with a body expected — not flagged)
head(:ok)

# good (dynamic status — not flagged to avoid false positives)
head(status_code)

Constant Summary collapse

MSG =
'Avoid `head(%<status>s)` for error handling. ' \
'Delegate to Rails exceptions or model validations instead.'.freeze
NO_CONTENT_MSG =
'Avoid `head(%<status>s)`: a bodyless response tells a JSON-driven ' \
'client nothing. Render an action response (even an empty one) instead.'.freeze
RESTRICT_ON_SEND =
%i[head].freeze
NO_CONTENT_STATUSES =
%w[no_content reset_content 204 205].freeze
DEFAULT_FLAGGED_STATUSES =
(%w[
  not_found unprocessable_entity forbidden unauthorized
  bad_request conflict gone method_not_allowed
  404 422 403 401 400 409 410 405
] + NO_CONTENT_STATUSES).freeze

Instance Method Summary collapse

Instance Method Details

#on_send(node) ⇒ Object



80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/rubocop/cop/dev_doc/style/avoid_head_response.rb', line 80

def on_send(node)
  # `self.head(...)` is the same controller call — a bare receiver
  # check would make explicit-self a one-token dodge.
  return unless node.receiver.nil? || node.receiver.self_type?

  status_node = node.arguments.first
  return unless status_node
  return unless flagged_literal?(status_node)

  template = NO_CONTENT_STATUSES.include?(status_node.value.to_s) ? NO_CONTENT_MSG : MSG
  add_offense(node.loc.selector, message: format(template, status: status_display(status_node)))
end