CrackedRuby CrackedRuby

HTTP Methods and Status Codes

Overview

HTTP methods and status codes form the foundation of client-server communication on the web. HTTP methods define the action a client wants to perform on a resource, while status codes communicate the result of that request. The HTTP/1.1 specification defines nine methods and organizes status codes into five classes, each serving specific purposes in the request-response cycle.

HTTP methods describe the desired action to perform on a resource identified by a URI. Each method has defined semantics that determine how servers should process requests and how clients should handle responses. The method indicates intent—whether the client wants to retrieve data, modify a resource, or remove it entirely. Safe methods make no modifications, while idempotent methods produce the same result regardless of how many times they execute.

Status codes appear in the first line of HTTP responses and indicate whether a request succeeded, failed, or requires additional action. The first digit determines the class: 1xx for informational responses, 2xx for success, 3xx for redirection, 4xx for client errors, and 5xx for server errors. Each code carries specific semantic meaning that clients use to determine how to proceed.

require 'net/http'

uri = URI('https://api.example.com/users/123')
response = Net::HTTP.get_response(uri)

puts "Status: #{response.code} #{response.message}"
# => Status: 200 OK

case response.code.to_i
when 200..299
  puts "Success: #{response.body}"
when 400..499
  puts "Client error"
when 500..599
  puts "Server error"
end

The relationship between methods and status codes determines API behavior. A GET request that finds a resource returns 200 OK, while the same request for a missing resource returns 404 Not Found. POST creates resources and typically returns 201 Created with a Location header, while PUT replaces existing resources and returns 200 OK or 204 No Content. DELETE removes resources and returns 204 No Content or 200 OK with a response body describing the deletion.

Key Principles

HTTP methods divide into categories based on their properties. Safe methods make no modifications to server state. GET, HEAD, OPTIONS, and TRACE are safe—clients can call them repeatedly without concern for side effects. This property allows caching, prefetching, and retrying without risk. Idempotent methods produce identical results when repeated. PUT, DELETE, GET, HEAD, OPTIONS, and TRACE are idempotent. Calling DELETE on a resource twice produces the same outcome as calling it once—the resource no longer exists. POST is neither safe nor idempotent because each request typically creates a new resource.

Cacheable methods allow responses to be stored and reused. GET, HEAD, and POST are cacheable, though POST caching requires explicit cache controls. Caching reduces server load and improves response times by serving stored responses for identical requests. Cache-Control headers determine how long responses remain valid and under what conditions clients must revalidate them.

The request-response cycle follows a defined pattern. Clients send requests containing a method, URI, headers, and optional body. Servers process the request, perform the indicated action, and return a status code with headers and optional body. The status code class indicates the general outcome, while the specific code provides details.

# Status code classes and their meanings
class HTTPResponse
  def self.status_class(code)
    case code
    when 100..199 then :informational
    when 200..299 then :success
    when 300..399 then :redirection
    when 400..499 then :client_error
    when 500..599 then :server_error
    else :unknown
    end
  end
end

HTTPResponse.status_class(200)  # => :success
HTTPResponse.status_class(404)  # => :client_error

Content negotiation determines response format. Clients send Accept headers indicating preferred media types, while servers respond with Content-Type headers specifying the actual format. A client requesting Accept: application/json receives JSON responses, while Accept: text/html receives HTML. Status code 406 Not Acceptable indicates the server cannot produce any acceptable format.

Conditional requests optimize transfers by using validators. ETag headers contain resource version identifiers, while Last-Modified headers indicate update times. Clients include If-None-Match or If-Modified-Since headers in subsequent requests. If the resource has not changed, servers return 304 Not Modified without a body, saving bandwidth. If-Match and If-Unmodified-Since enable optimistic locking for updates.

require 'net/http'

uri = URI('https://api.example.com/data')
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true

# Initial request
request = Net::HTTP::Get.new(uri)
response = http.request(request)
etag = response['ETag']
data = response.body

# Conditional request
request = Net::HTTP::Get.new(uri)
request['If-None-Match'] = etag
response = http.request(request)

if response.code == '304'
  puts "Using cached data"
  # Use previously stored data
else
  puts "Data changed, updating cache"
  data = response.body
end

Method semantics define expected behavior. GET retrieves representations without modification. HEAD retrieves only headers, useful for checking resource existence or metadata without transferring the body. POST submits data for processing, often creating new resources. PUT replaces entire resources with provided representations. PATCH applies partial modifications. DELETE removes resources. OPTIONS describes communication options for resources. TRACE performs message loop-back tests. CONNECT establishes tunnels for secure communication.

Status code semantics convey precise outcomes. 200 OK indicates success with a response body. 201 Created signals successful resource creation, typically including a Location header pointing to the new resource. 204 No Content indicates success without a response body. 301 Moved Permanently redirects permanently with a Location header. 302 Found redirects temporarily. 400 Bad Request signals malformed requests. 401 Unauthorized requires authentication. 403 Forbidden denies access regardless of authentication. 404 Not Found indicates missing resources. 500 Internal Server Error reports unrecoverable server failures. 503 Service Unavailable indicates temporary unavailability, often with a Retry-After header.

Ruby Implementation

Ruby provides multiple approaches for HTTP communication. The standard library includes Net::HTTP for basic HTTP operations, while popular gems offer higher-level abstractions. Web frameworks like Rails and Sinatra build on these foundations to simplify API development.

Net::HTTP handles low-level HTTP operations. It supports all standard methods and provides access to headers, status codes, and response bodies. The library requires manual connection management and lacks advanced features like automatic redirects or connection pooling.

require 'net/http'
require 'json'

# GET request
uri = URI('https://api.example.com/users')
response = Net::HTTP.get_response(uri)
users = JSON.parse(response.body) if response.code == '200'

# POST request with JSON payload
uri = URI('https://api.example.com/users')
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true

request = Net::HTTP::Post.new(uri.path)
request['Content-Type'] = 'application/json'
request.body = JSON.generate({
  name: 'Alice',
  email: 'alice@example.com'
})

response = http.request(request)
case response.code
when '201'
  new_user = JSON.parse(response.body)
  location = response['Location']
  puts "Created user at #{location}"
when '400'
  errors = JSON.parse(response.body)
  puts "Validation errors: #{errors}"
when '500'
  puts "Server error"
end

HTTP.rb gem provides a chainable interface with connection pooling and automatic redirects. The API emphasizes immutability and composability, making request construction more expressive.

require 'http'

# GET with query parameters
response = HTTP.get('https://api.example.com/search', params: {
  q: 'ruby',
  limit: 10
})

if response.status.success?
  results = response.parse
end

# POST with authentication
response = HTTP.auth("Bearer #{token}")
              .post('https://api.example.com/data', json: {
                key: 'value'
              })

# Handle different status codes
case response.code
when 200..299
  data = response.parse
when 401
  # Refresh token and retry
when 404
  # Resource not found
when 500..599
  # Server error, implement retry with backoff
end

Faraday abstracts HTTP clients through a middleware architecture. Adapters support different backends, while middleware handles concerns like logging, caching, and retry logic.

require 'faraday'
require 'faraday/retry'

conn = Faraday.new(url: 'https://api.example.com') do |f|
  f.request :json
  f.request :retry, max: 3, interval: 0.5,
                    retry_statuses: [500, 502, 503, 504]
  f.response :json
  f.response :raise_error
  f.adapter Faraday.default_adapter
end

begin
  # GET request
  response = conn.get('/users/123')
  user = response.body
  
  # PUT request - replace entire resource
  response = conn.put('/users/123') do |req|
    req.body = {
      name: 'Bob',
      email: 'bob@example.com',
      role: 'admin'
    }
  end
  
  # PATCH request - partial update
  response = conn.patch('/users/123') do |req|
    req.body = { role: 'moderator' }
  end
  
  # DELETE request
  response = conn.delete('/users/123')
  puts "Deleted" if response.status == 204

rescue Faraday::ResourceNotFound => e
  puts "User not found: #{e.response[:status]}"
rescue Faraday::ServerError => e
  puts "Server error: #{e.response[:status]}"
end

Rails controllers map HTTP methods to controller actions through routing. REST conventions associate methods with CRUD operations—GET for read, POST for create, PUT/PATCH for update, DELETE for destroy. The framework automatically parses request bodies and sets appropriate status codes.

class UsersController < ApplicationController
  # GET /users
  def index
    @users = User.all
    render json: @users, status: :ok
  end

  # GET /users/:id
  def show
    @user = User.find_by(id: params[:id])
    if @user
      render json: @user, status: :ok
    else
      render json: { error: 'Not found' }, status: :not_found
    end
  end

  # POST /users
  def create
    @user = User.new(user_params)
    if @user.save
      render json: @user, status: :created, location: @user
    else
      render json: { errors: @user.errors }, status: :unprocessable_entity
    end
  end

  # PUT /users/:id - replace entire resource
  def update
    @user = User.find_by(id: params[:id])
    return render json: { error: 'Not found' }, status: :not_found unless @user
    
    if @user.update(user_params)
      render json: @user, status: :ok
    else
      render json: { errors: @user.errors }, status: :unprocessable_entity
    end
  end

  # PATCH /users/:id - partial update
  def update
    @user = User.find_by(id: params[:id])
    return render json: { error: 'Not found' }, status: :not_found unless @user
    
    if @user.update(user_params)
      head :no_content  # 204 No Content
    else
      render json: { errors: @user.errors }, status: :unprocessable_entity
    end
  end

  # DELETE /users/:id
  def destroy
    @user = User.find_by(id: params[:id])
    return head :not_found unless @user
    
    @user.destroy
    head :no_content  # 204 No Content
  end

  private

  def user_params
    params.require(:user).permit(:name, :email, :role)
  end
end

Sinatra provides minimal routing with explicit method handlers. Each route specifies the HTTP method, path pattern, and handler block. The framework exposes status codes and headers through helper methods.

require 'sinatra'
require 'json'

# GET route
get '/users/:id' do
  user = User.find(params[:id])
  if user
    content_type :json
    status 200
    user.to_json
  else
    status 404
    { error: 'User not found' }.to_json
  end
end

# POST route
post '/users' do
  data = JSON.parse(request.body.read)
  user = User.new(data)
  
  if user.save
    status 201
    headers 'Location' => "/users/#{user.id}"
    user.to_json
  else
    status 422
    { errors: user.errors }.to_json
  end
end

# PUT route - full replacement
put '/users/:id' do
  user = User.find(params[:id])
  halt 404, { error: 'Not found' }.to_json unless user
  
  data = JSON.parse(request.body.read)
  if user.update(data)
    status 200
    user.to_json
  else
    status 422
    { errors: user.errors }.to_json
  end
end

# DELETE route
delete '/users/:id' do
  user = User.find(params[:id])
  halt 404 unless user
  
  user.destroy
  status 204  # No Content
end

# HEAD route - check resource existence
head '/users/:id' do
  user = User.find(params[:id])
  status user ? 200 : 404
end

# OPTIONS route - describe available methods
options '/users/:id' do
  headers 'Allow' => 'GET, PUT, PATCH, DELETE, HEAD, OPTIONS'
  status 200
end

Custom status code handling requires mapping codes to application logic. Creating a status handler module centralizes this logic and ensures consistent responses.

module StatusHandler
  def handle_response(response)
    case response.code.to_i
    when 200..299
      success_handler(response)
    when 300..399
      redirect_handler(response)
    when 400..499
      client_error_handler(response)
    when 500..599
      server_error_handler(response)
    else
      unknown_handler(response)
    end
  end

  private

  def success_handler(response)
    case response.code.to_i
    when 200
      { status: :ok, data: parse_body(response) }
    when 201
      { status: :created, location: response['Location'] }
    when 204
      { status: :no_content }
    else
      { status: :success, code: response.code }
    end
  end

  def redirect_handler(response)
    location = response['Location']
    case response.code.to_i
    when 301
      { status: :moved_permanently, location: location }
    when 302, 303
      { status: :found, location: location }
    when 304
      { status: :not_modified }
    else
      { status: :redirect, location: location }
    end
  end

  def client_error_handler(response)
    case response.code.to_i
    when 400
      { status: :bad_request, errors: parse_errors(response) }
    when 401
      { status: :unauthorized, message: 'Authentication required' }
    when 403
      { status: :forbidden, message: 'Access denied' }
    when 404
      { status: :not_found, message: 'Resource not found' }
    when 409
      { status: :conflict, errors: parse_errors(response) }
    when 422
      { status: :unprocessable_entity, errors: parse_errors(response) }
    when 429
      retry_after = response['Retry-After']
      { status: :too_many_requests, retry_after: retry_after }
    else
      { status: :client_error, code: response.code }
    end
  end

  def server_error_handler(response)
    { status: :server_error, code: response.code, message: response.message }
  end

  def parse_body(response)
    JSON.parse(response.body) rescue response.body
  end

  def parse_errors(response)
    parsed = JSON.parse(response.body)
    parsed['errors'] || parsed
  rescue JSON::ParserError
    { message: response.body }
  end
end

Practical Examples

RESTful API client demonstrates comprehensive method usage. The client handles authentication, retries, and different response scenarios.

require 'net/http'
require 'json'
require 'uri'

class APIClient
  def initialize(base_url, api_key)
    @base_url = base_url
    @api_key = api_key
    @uri = URI(base_url)
  end

  def list_resources(path, params = {})
    query = URI.encode_www_form(params)
    uri = URI("#{@base_url}#{path}?#{query}")
    
    request = Net::HTTP::Get.new(uri)
    add_auth_header(request)
    
    response = execute_request(uri, request)
    
    case response.code
    when '200'
      JSON.parse(response.body)
    when '401'
      raise AuthenticationError, 'Invalid credentials'
    when '404'
      []  # Return empty array for missing collections
    else
      handle_error(response)
    end
  end

  def get_resource(path, id)
    uri = URI("#{@base_url}#{path}/#{id}")
    request = Net::HTTP::Get.new(uri)
    add_auth_header(request)
    
    response = execute_request(uri, request)
    
    case response.code
    when '200'
      JSON.parse(response.body)
    when '304'
      nil  # Not modified, use cache
    when '404'
      raise ResourceNotFound, "Resource #{id} not found"
    when '410'
      raise ResourceGone, "Resource #{id} permanently deleted"
    else
      handle_error(response)
    end
  end

  def create_resource(path, data)
    uri = URI("#{@base_url}#{path}")
    request = Net::HTTP::Post.new(uri)
    add_auth_header(request)
    request['Content-Type'] = 'application/json'
    request.body = JSON.generate(data)
    
    response = execute_request(uri, request)
    
    case response.code
    when '201'
      location = response['Location']
      { id: extract_id(location), location: location, data: JSON.parse(response.body) }
    when '400'
      errors = JSON.parse(response.body)
      raise ValidationError.new('Invalid data', errors)
    when '409'
      raise ConflictError, 'Resource already exists'
    when '422'
      errors = JSON.parse(response.body)
      raise ValidationError.new('Unprocessable data', errors)
    else
      handle_error(response)
    end
  end

  def update_resource(path, id, data)
    uri = URI("#{@base_url}#{path}/#{id}")
    request = Net::HTTP::Put.new(uri)
    add_auth_header(request)
    request['Content-Type'] = 'application/json'
    request.body = JSON.generate(data)
    
    response = execute_request(uri, request)
    
    case response.code
    when '200'
      JSON.parse(response.body)
    when '204'
      nil  # No content returned
    when '404'
      raise ResourceNotFound, "Resource #{id} not found"
    when '409'
      raise ConflictError, 'Update conflict, resource modified'
    when '412'
      raise PreconditionFailed, 'Precondition failed, resource changed'
    else
      handle_error(response)
    end
  end

  def partial_update(path, id, changes)
    uri = URI("#{@base_url}#{path}/#{id}")
    request = Net::HTTP::Patch.new(uri)
    add_auth_header(request)
    request['Content-Type'] = 'application/json'
    request.body = JSON.generate(changes)
    
    response = execute_request(uri, request)
    
    case response.code
    when '200'
      JSON.parse(response.body)
    when '204'
      nil
    when '404'
      raise ResourceNotFound
    else
      handle_error(response)
    end
  end

  def delete_resource(path, id)
    uri = URI("#{@base_url}#{path}/#{id}")
    request = Net::HTTP::Delete.new(uri)
    add_auth_header(request)
    
    response = execute_request(uri, request)
    
    case response.code
    when '200'
      JSON.parse(response.body)  # Deleted resource representation
    when '204'
      true  # Successful deletion, no content
    when '404'
      false  # Already deleted or never existed
    when '409'
      raise ConflictError, 'Cannot delete, resource in use'
    else
      handle_error(response)
    end
  end

  def resource_exists?(path, id)
    uri = URI("#{@base_url}#{path}/#{id}")
    request = Net::HTTP::Head.new(uri)
    add_auth_header(request)
    
    response = execute_request(uri, request)
    response.code == '200'
  end

  def get_options(path)
    uri = URI("#{@base_url}#{path}")
    request = Net::HTTP::Options.new(uri)
    add_auth_header(request)
    
    response = execute_request(uri, request)
    
    if response.code == '200'
      {
        allow: response['Allow']&.split(',')&.map(&:strip),
        content_types: response['Accept']&.split(',')&.map(&:strip)
      }
    else
      handle_error(response)
    end
  end

  private

  def execute_request(uri, request)
    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = uri.scheme == 'https'
    http.request(request)
  end

  def add_auth_header(request)
    request['Authorization'] = "Bearer #{@api_key}"
  end

  def extract_id(location)
    location.split('/').last
  end

  def handle_error(response)
    case response.code.to_i
    when 500..599
      raise ServerError, "Server error: #{response.code}"
    else
      raise HTTPError, "HTTP error: #{response.code} #{response.message}"
    end
  end

  class AuthenticationError < StandardError; end
  class ResourceNotFound < StandardError; end
  class ResourceGone < StandardError; end
  class ValidationError < StandardError
    attr_reader :errors
    def initialize(message, errors = {})
      super(message)
      @errors = errors
    end
  end
  class ConflictError < StandardError; end
  class PreconditionFailed < StandardError; end
  class ServerError < StandardError; end
  class HTTPError < StandardError; end
end

# Usage
client = APIClient.new('https://api.example.com', 'api_key_123')

# List with pagination
users = client.list_resources('/users', page: 1, limit: 20)

# Get single resource
user = client.get_resource('/users', 123)

# Create
new_user = client.create_resource('/users', {
  name: 'Alice',
  email: 'alice@example.com'
})
puts "Created at: #{new_user[:location]}"

# Full update
client.update_resource('/users', 123, {
  name: 'Alice Smith',
  email: 'alice@example.com',
  role: 'admin'
})

# Partial update
client.partial_update('/users', 123, { role: 'moderator' })

# Delete
deleted = client.delete_resource('/users', 123)

# Check existence
exists = client.resource_exists?('/users', 123)

# Get supported methods
options = client.get_options('/users')
puts "Supported methods: #{options[:allow]}"

Conditional request handling optimizes bandwidth by using ETags and modification times.

class CachedAPIClient
  def initialize(base_url)
    @base_url = base_url
    @cache = {}
  end

  def get_with_cache(path)
    cache_key = path
    cached = @cache[cache_key]
    
    uri = URI("#{@base_url}#{path}")
    request = Net::HTTP::Get.new(uri)
    
    if cached
      # Add conditional headers
      request['If-None-Match'] = cached[:etag] if cached[:etag]
      request['If-Modified-Since'] = cached[:last_modified] if cached[:last_modified]
    end
    
    response = execute_request(uri, request)
    
    case response.code
    when '200'
      # Update cache
      @cache[cache_key] = {
        data: JSON.parse(response.body),
        etag: response['ETag'],
        last_modified: response['Last-Modified'],
        expires: response['Expires']
      }
      @cache[cache_key][:data]
      
    when '304'
      # Use cached data
      puts "Cache hit: using stored data"
      cached[:data]
      
    when '404'
      # Remove from cache if previously existed
      @cache.delete(cache_key)
      raise ResourceNotFound
      
    else
      handle_error(response)
    end
  end

  def update_with_precondition(path, id, data, etag)
    uri = URI("#{@base_url}#{path}/#{id}")
    request = Net::HTTP::Put.new(uri)
    request['Content-Type'] = 'application/json'
    request['If-Match'] = etag  # Optimistic locking
    request.body = JSON.generate(data)
    
    response = execute_request(uri, request)
    
    case response.code
    when '200', '204'
      # Update successful
      @cache.delete("#{path}/#{id}")
      true
      
    when '412'
      # Precondition failed - resource modified
      puts "Resource changed, cannot update"
      false
      
    when '404'
      raise ResourceNotFound
      
    else
      handle_error(response)
    end
  end

  private

  def execute_request(uri, request)
    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = uri.scheme == 'https'
    http.request(request)
  end
end

# Usage
client = CachedAPIClient.new('https://api.example.com')

# First request - cache miss
data = client.get_with_cache('/users/123')

# Second request - returns 304, uses cache
data = client.get_with_cache('/users/123')

# Update with optimistic locking
etag = 'W/"abc123"'
success = client.update_with_precondition('/users', 123, new_data, etag)

Rate limiting and retry handles 429 Too Many Requests and temporary server errors.

require 'net/http'

class RateLimitedClient
  MAX_RETRIES = 3
  
  def initialize(base_url)
    @base_url = base_url
  end

  def request_with_retry(method, path, body: nil, retries: 0)
    uri = URI("#{@base_url}#{path}")
    request = build_request(method, uri, body)
    
    response = execute_request(uri, request)
    
    case response.code
    when '200', '201', '204'
      handle_success(response)
      
    when '429'
      handle_rate_limit(response, method, path, body, retries)
      
    when '503'
      handle_service_unavailable(response, method, path, body, retries)
      
    when '500', '502', '504'
      handle_server_error(response, method, path, body, retries)
      
    else
      raise HTTPError, "HTTP #{response.code}: #{response.message}"
    end
  end

  private

  def build_request(method, uri, body)
    request = case method.upcase
    when 'GET' then Net::HTTP::Get.new(uri)
    when 'POST' then Net::HTTP::Post.new(uri)
    when 'PUT' then Net::HTTP::Put.new(uri)
    when 'DELETE' then Net::HTTP::Delete.new(uri)
    end
    
    if body
      request['Content-Type'] = 'application/json'
      request.body = JSON.generate(body)
    end
    
    request
  end

  def execute_request(uri, request)
    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = uri.scheme == 'https'
    http.request(request)
  end

  def handle_success(response)
    return nil if response.code == '204'
    JSON.parse(response.body) if response.body && !response.body.empty?
  end

  def handle_rate_limit(response, method, path, body, retries)
    if retries >= MAX_RETRIES
      raise RateLimitError, "Rate limit exceeded after #{MAX_RETRIES} retries"
    end
    
    # Check Retry-After header
    retry_after = response['Retry-After']
    wait_time = if retry_after
      # Can be seconds or HTTP date
      retry_after.to_i > 0 ? retry_after.to_i : 60
    else
      calculate_backoff(retries)
    end
    
    puts "Rate limited, waiting #{wait_time} seconds..."
    sleep(wait_time)
    request_with_retry(method, path, body: body, retries: retries + 1)
  end

  def handle_service_unavailable(response, method, path, body, retries)
    if retries >= MAX_RETRIES
      raise ServiceUnavailable, "Service unavailable after #{MAX_RETRIES} retries"
    end
    
    retry_after = response['Retry-After']&.to_i || calculate_backoff(retries)
    puts "Service unavailable, retrying in #{retry_after} seconds..."
    sleep(retry_after)
    request_with_retry(method, path, body: body, retries: retries + 1)
  end

  def handle_server_error(response, method, path, body, retries)
    # Don't retry non-idempotent methods unless explicitly safe
    unless %w[GET HEAD PUT DELETE OPTIONS].include?(method.upcase)
      raise ServerError, "Server error #{response.code}, cannot retry non-idempotent #{method}"
    end
    
    if retries >= MAX_RETRIES
      raise ServerError, "Server error after #{MAX_RETRIES} retries"
    end
    
    wait_time = calculate_backoff(retries)
    puts "Server error #{response.code}, retrying in #{wait_time} seconds..."
    sleep(wait_time)
    request_with_retry(method, path, body: body, retries: retries + 1)
  end

  def calculate_backoff(retry_count)
    # Exponential backoff: 1s, 2s, 4s
    2 ** retry_count
  end

  class RateLimitError < StandardError; end
  class ServiceUnavailable < StandardError; end
  class ServerError < StandardError; end
  class HTTPError < StandardError; end
end

# Usage
client = RateLimitedClient.new('https://api.example.com')

begin
  data = client.request_with_retry('GET', '/users/123')
  
  client.request_with_retry('POST', '/users', body: {
    name: 'Alice',
    email: 'alice@example.com'
  })
rescue RateLimitedClient::RateLimitError => e
  puts "Still rate limited after retries: #{e.message}"
rescue RateLimitedClient::ServiceUnavailable => e
  puts "Service down: #{e.message}"
end

Security Implications

HTTP methods carry security implications based on their semantics and typical usage patterns. Understanding these implications prevents vulnerabilities in API design and implementation.

Method-based access control restricts operations based on HTTP methods. A user might have permission to GET resources but not DELETE them. Servers must validate permissions for each method independently, not just check resource access. Failing to enforce method-level permissions allows privilege escalation where users perform unauthorized actions on accessible resources.

class SecureController < ApplicationController
  before_action :authenticate_user!
  before_action :authorize_action

  def show
    # GET - read access
    @resource = Resource.find(params[:id])
    render json: @resource
  end

  def update
    # PUT/PATCH - write access
    @resource = Resource.find(params[:id])
    @resource.update(resource_params)
    render json: @resource
  end

  def destroy
    # DELETE - delete access
    @resource = Resource.find(params[:id])
    @resource.destroy
    head :no_content
  end

  private

  def authorize_action
    permission = case request.method
    when 'GET', 'HEAD' then :read
    when 'POST' then :create
    when 'PUT', 'PATCH' then :write
    when 'DELETE' then :delete
    else :none
    end

    unless current_user.can?(permission, resource_type)
      render json: { error: 'Forbidden' }, status: :forbidden
    end
  end

  def resource_type
    controller_name.singularize
  end
end

Safe method assumptions create vulnerabilities when violated. Applications that modify state in GET handlers break caching assumptions and enable CSRF attacks. GET requests appear in browser history, server logs, and referrer headers. Placing sensitive data or state-changing operations in GET URLs exposes them to logging and leakage. Attackers can craft malicious links that trigger actions when victims visit them.

# VULNERABLE - state change in GET
class VulnerableController < ApplicationController
  def delete_account
    # DO NOT DO THIS - destructive action in GET
    if params[:confirm] == 'yes'
      current_user.destroy
      redirect_to root_path
    end
  end
end

# SECURE - state change requires POST/DELETE
class SecureController < ApplicationController
  protect_from_forgery with: :exception

  def delete_account
    # Show confirmation page for GET
    render :confirm_deletion
  end

  def destroy_account
    # Actual deletion requires POST/DELETE with CSRF token
    current_user.destroy
    redirect_to root_path
  end
end

CSRF protection prevents cross-site request forgery. Safe methods (GET, HEAD, OPTIONS) require no CSRF protection because they make no modifications. Unsafe methods (POST, PUT, PATCH, DELETE) must include CSRF tokens that attackers cannot obtain. Rails includes CSRF protection by default, but developers must not disable it for unsafe methods.

class ProtectedController < ApplicationController
  # CSRF protection enabled by default for non-GET
  protect_from_forgery with: :exception

  # Skip CSRF for API endpoints that use token authentication
  skip_before_action :verify_authenticity_token, if: :api_request?

  def create
    # POST protected by CSRF token
    @resource = Resource.create(resource_params)
    render json: @resource, status: :created
  end

  def update
    # PUT protected by CSRF token
    @resource = Resource.find(params[:id])
    @resource.update(resource_params)
    render json: @resource
  end

  private

  def api_request?
    # API requests use Bearer tokens instead of session cookies
    request.headers['Authorization']&.start_with?('Bearer ')
  end
end

# API client bypasses CSRF using Bearer token
class APIAuthController < ApplicationController
  skip_before_action :verify_authenticity_token
  before_action :authenticate_token

  private

  def authenticate_token
    token = request.headers['Authorization']&.sub(/^Bearer /, '')
    @current_user = User.find_by(api_token: token)
    
    unless @current_user
      render json: { error: 'Unauthorized' }, status: :unauthorized
    end
  end
end

HTTP method override allows clients to tunnel PUT, PATCH, and DELETE through POST when infrastructure blocks them. This creates security risks if not properly validated. Attackers can bypass method-based security checks by sending POST with a method override header.

class MethodOverrideController < ApplicationController
  # Rails handles _method parameter automatically
  # But validate it doesn't bypass security
  
  before_action :validate_method_override

  private

  def validate_method_override
    # Ensure method override doesn't bypass authentication
    if params[:_method].present?
      actual_method = params[:_method].upcase
      
      # Don't allow method override to escalate privileges
      unless request.post? || request.put? || request.patch?
        render json: { error: 'Method override only allowed for POST' },
               status: :method_not_allowed
      end
      
      # Verify user has permission for the overridden method
      unless authorized_for_method?(actual_method)
        render json: { error: 'Forbidden' }, status: :forbidden
      end
    end
  end
end

Status code information leakage occurs when error responses reveal system details. A 404 Not Found tells attackers a resource does not exist, while 403 Forbidden confirms it exists but access is denied. For unauthorized access, returning 404 instead of 403 hides resource existence.

class InformationLeakController < ApplicationController
  def show
    @resource = Resource.find_by(id: params[:id])
    
    # INFORMATION LEAK - reveals whether resource exists
    unless @resource
      render json: { error: 'Not found' }, status: :not_found
      return
    end
    
    unless current_user.can_view?(@resource)
      render json: { error: 'Forbidden' }, status: :forbidden
      return
    end
    
    render json: @resource
  end
end

class SecureController < ApplicationController
  def show
    @resource = Resource.find_by(id: params[:id])
    
    # Hide resource existence for unauthorized access
    if @resource.nil? || !current_user.can_view?(@resource)
      render json: { error: 'Not found' }, status: :not_found
      return
    end
    
    render json: @resource
  end
end

Authentication requirements differ by method. Some APIs allow anonymous GET but require authentication for POST, PUT, DELETE. Authentication checks must occur before authorization checks and apply consistently across all methods.

class AuthenticatedController < ApplicationController
  # Allow anonymous read access
  before_action :authenticate_user!, except: [:index, :show]
  before_action :authorize_write, only: [:create, :update, :destroy]

  def index
    # Public - no authentication
    @resources = Resource.public_scope
    render json: @resources
  end

  def show
    # Public - no authentication
    @resource = Resource.public_scope.find(params[:id])
    render json: @resource
  end

  def create
    # Requires authentication and authorization
    @resource = current_user.resources.create(resource_params)
    render json: @resource, status: :created
  end

  def update
    # Requires authentication and ownership
    @resource = current_user.resources.find(params[:id])
    @resource.update(resource_params)
    render json: @resource
  end

  def destroy
    # Requires authentication and ownership
    @resource = current_user.resources.find(params[:id])
    @resource.destroy
    head :no_content
  end

  private

  def authorize_write
    unless current_user.can_write?
      render json: { error: 'Insufficient permissions' },
             status: :forbidden
    end
  end
end

Common Patterns

RESTful resource mapping associates HTTP methods with CRUD operations. This pattern creates consistent, predictable APIs where method semantics match operation intent.

# RESTful routes in Rails
# GET    /articles          -> index   (list all)
# GET    /articles/new      -> new     (form for creating)
# POST   /articles          -> create  (create resource)
# GET    /articles/:id      -> show    (display resource)
# GET    /articles/:id/edit -> edit    (form for editing)
# PUT    /articles/:id      -> update  (replace resource)
# PATCH  /articles/:id      -> update  (modify resource)
# DELETE /articles/:id      -> destroy (delete resource)

class ArticlesController < ApplicationController
  # Collection endpoints
  def index
    @articles = Article.all
    render json: @articles
  end

  def create
    @article = Article.new(article_params)
    if @article.save
      render json: @article, status: :created, location: @article
    else
      render json: { errors: @article.errors }, status: :unprocessable_entity
    end
  end

  # Member endpoints
  def show
    @article = Article.find(params[:id])
    render json: @article
  end

  def update
    @article = Article.find(params[:id])
    if @article.update(article_params)
      render json: @article
    else
      render json: { errors: @article.errors }, status: :unprocessable_entity
    end
  end

  def destroy
    @article = Article.find(params[:id])
    @article.destroy
    head :no_content
  end

  private

  def article_params
    params.require(:article).permit(:title, :body, :published)
  end
end

Nested resource patterns represent relationships through URI hierarchies. Methods operate within the context of parent resources.

# Nested routes
# GET    /articles/:article_id/comments
# POST   /articles/:article_id/comments
# GET    /articles/:article_id/comments/:id
# PATCH  /articles/:article_id/comments/:id
# DELETE /articles/:article_id/comments/:id

class CommentsController < ApplicationController
  before_action :load_article

  def index
    @comments = @article.comments
    render json: @comments
  end

  def create
    @comment = @article.comments.build(comment_params)
    if @comment.save
      render json: @comment, status: :created,
             location: [@article, @comment]
    else
      render json: { errors: @comment.errors },
             status: :unprocessable_entity
    end
  end

  def show
    @comment = @article.comments.find(params[:id])
    render json: @comment
  end

  def update
    @comment = @article.comments.find(params[:id])
    if @comment.update(comment_params)
      render json: @comment
    else
      render json: { errors: @comment.errors },
             status: :unprocessable_entity
    end
  end

  def destroy
    @comment = @article.comments.find(params[:id])
    @comment.destroy
    head :no_content
  end

  private

  def load_article
    @article = Article.find(params[:article_id])
  rescue ActiveRecord::RecordNotFound
    render json: { error: 'Article not found' }, status: :not_found
  end

  def comment_params
    params.require(:comment).permit(:body, :author)
  end
end

Bulk operations handle multiple resources efficiently. POST to a collection endpoint can create multiple items, while custom endpoints handle bulk updates or deletes.

class BulkOperationsController < ApplicationController
  # POST /articles/bulk - create multiple articles
  def bulk_create
    results = []
    errors = []
    
    params[:articles].each_with_index do |article_data, index|
      article = Article.new(article_data.permit(:title, :body))
      if article.save
        results << { index: index, id: article.id, status: :created }
      else
        errors << { index: index, errors: article.errors }
      end
    end
    
    if errors.empty?
      render json: { results: results }, status: :created
    else
      render json: { results: results, errors: errors },
             status: :multi_status
    end
  end

  # PATCH /articles/bulk - update multiple articles
  def bulk_update
    results = []
    params[:updates].each do |update_data|
      article = Article.find_by(id: update_data[:id])
      if article && article.update(update_data.permit(:title, :body))
        results << { id: article.id, status: :updated }
      else
        results << { id: update_data[:id], status: :failed }
      end
    end
    
    render json: { results: results }
  end

  # POST /articles/bulk_delete - delete multiple articles
  def bulk_delete
    ids = params[:ids]
    deleted = Article.where(id: ids).destroy_all
    
    render json: {
      deleted_count: deleted.count,
      deleted_ids: deleted.map(&:id)
    }
  end
end

Pagination patterns use query parameters with Link headers. Clients request pages through GET parameters, while responses include metadata and navigation links.

class PaginatedController < ApplicationController
  def index
    page = params[:page]&.to_i || 1
    per_page = [params[:per_page]&.to_i || 20, 100].min
    
    @resources = Resource.page(page).per(per_page)
    
    # Add Link header for pagination
    add_pagination_headers(@resources)
    
    render json: {
      data: @resources,
      meta: {
        current_page: page,
        per_page: per_page,
        total_pages: @resources.total_pages,
        total_count: @resources.total_count
      }
    }
  end

  private

  def add_pagination_headers(collection)
    links = []
    base_url = request.base_url + request.path
    
    # First page
    links << "<#{base_url}?page=1&per_page=#{collection.limit_value}>; rel=\"first\""
    
    # Previous page
    if collection.prev_page
      links << "<#{base_url}?page=#{collection.prev_page}&per_page=#{collection.limit_value}>; rel=\"prev\""
    end
    
    # Next page
    if collection.next_page
      links << "<#{base_url}?page=#{collection.next_page}&per_page=#{collection.limit_value}>; rel=\"next\""
    end
    
    # Last page
    links << "<#{base_url}?page=#{collection.total_pages}&per_page=#{collection.limit_value}>; rel=\"last\""
    
    response.headers['Link'] = links.join(', ')
  end
end

Idempotency keys prevent duplicate processing of non-idempotent POST requests. Clients include unique keys in headers, and servers track processed requests.

class IdempotentController < ApplicationController
  before_action :check_idempotency_key, only: [:create]

  def create
    # Process request only once per idempotency key
    @resource = Resource.create(resource_params)
    
    # Store result for this idempotency key
    Rails.cache.write(
      idempotency_cache_key,
      { status: :created, id: @resource.id, data: @resource.as_json },
      expires_in: 24.hours
    )
    
    render json: @resource, status: :created
  end

  private

  def check_idempotency_key
    @idempotency_key = request.headers['Idempotency-Key']
    
    unless @idempotency_key
      render json: { error: 'Idempotency-Key header required' },
             status: :bad_request
      return
    end
    
    # Check if we've already processed this key
    cached_result = Rails.cache.read(idempotency_cache_key)
    
    if cached_result
      # Return cached response
      render json: cached_result[:data], status: cached_result[:status]
    end
  end

  def idempotency_cache_key
    "idempotency:#{@idempotency_key}"
  end
end

Status code handling patterns create consistent error responses across an application.

class ApplicationController < ActionController::API
  rescue_from ActiveRecord::RecordNotFound, with: :not_found
  rescue_from ActiveRecord::RecordInvalid, with: :unprocessable_entity
  rescue_from ActionController::ParameterMissing, with: :bad_request

  private

  def not_found(exception)
    render json: {
      error: 'Not Found',
      message: exception.message
    }, status: :not_found
  end

  def unprocessable_entity(exception)
    render json: {
      error: 'Unprocessable Entity',
      errors: exception.record.errors.full_messages
    }, status: :unprocessable_entity
  end

  def bad_request(exception)
    render json: {
      error: 'Bad Request',
      message: exception.message
    }, status: :bad_request
  end
end

Error Handling & Edge Cases

Network timeouts occur when servers fail to respond within expected timeframes. Clients must set appropriate timeouts and handle timeout errors gracefully.

require 'net/http'
require 'timeout'

class TimeoutHandler
  DEFAULT_TIMEOUT = 30
  
  def initialize(base_url, timeout: DEFAULT_TIMEOUT)
    @base_url = base_url
    @timeout = timeout
  end

  def request(method, path, body: nil)
    uri = URI("#{@base_url}#{path}")
    
    begin
      Timeout.timeout(@timeout) do
        http = Net::HTTP.new(uri.host, uri.port)
        http.use_ssl = uri.scheme == 'https'
        http.open_timeout = @timeout / 3  # Connection timeout
        http.read_timeout = @timeout       # Read timeout
        
        request = build_request(method, uri, body)
        response = http.request(request)
        
        handle_response(response)
      end
    rescue Timeout::Error
      raise RequestTimeout, "Request timed out after #{@timeout} seconds"
    rescue Net::OpenTimeout
      raise ConnectionTimeout, "Connection timed out"
    rescue Net::ReadTimeout
      raise ReadTimeout, "Read timed out"
    rescue Errno::ECONNREFUSED
      raise ConnectionRefused, "Connection refused"
    rescue SocketError => e
      raise NetworkError, "Network error: #{e.message}"
    end
  end

  private

  def build_request(method, uri, body)
    request_class = case method.upcase
    when 'GET' then Net::HTTP::Get
    when 'POST' then Net::HTTP::Post
    when 'PUT' then Net::HTTP::Put
    when 'DELETE' then Net::HTTP::Delete
    else raise ArgumentError, "Unsupported method: #{method}"
    end
    
    request = request_class.new(uri)
    
    if body
      request['Content-Type'] = 'application/json'
      request.body = JSON.generate(body)
    end
    
    request
  end

  def handle_response(response)
    case response.code.to_i
    when 200..299
      JSON.parse(response.body) if response.body
    when 408
      raise RequestTimeout, "Server reported request timeout"
    when 504
      raise GatewayTimeout, "Gateway timeout"
    else
      raise HTTPError, "HTTP #{response.code}: #{response.message}"
    end
  end

  class RequestTimeout < StandardError; end
  class ConnectionTimeout < StandardError; end
  class ReadTimeout < StandardError; end
  class ConnectionRefused < StandardError; end
  class NetworkError < StandardError; end
  class GatewayTimeout < StandardError; end
  class HTTPError < StandardError; end
end

Malformed responses require defensive parsing. Servers may return invalid JSON, missing headers, or unexpected content types.

class SafeResponseParser
  def parse_response(response)
    # Verify status code is valid
    status_code = response.code.to_i
    unless (100..599).include?(status_code)
      raise InvalidResponse, "Invalid status code: #{response.code}"
    end
    
    # Check content type
    content_type = response['Content-Type']
    
    case status_code
    when 204
      # No content expected
      return nil
      
    when 205
      # Reset content, no body
      return nil
      
    when 304
      # Not modified, no body
      return nil
      
    else
      # Parse body based on content type
      parse_body(response, content_type)
    end
  end

  private

  def parse_body(response, content_type)
    return nil if response.body.nil? || response.body.empty?
    
    case content_type
    when /application\/json/
      parse_json(response.body)
    when /text\/html/
      response.body  # Return as string
    when /application\/xml/
      parse_xml(response.body)
    else
      # Unknown content type
      response.body
    end
  rescue => e
    raise ParseError, "Failed to parse response: #{e.message}"
  end

  def parse_json(body)
    JSON.parse(body)
  rescue JSON::ParserError => e
    raise ParseError, "Invalid JSON: #{e.message}"
  end

  def parse_xml(body)
    # XML parsing implementation
    body
  end

  class InvalidResponse < StandardError; end
  class ParseError < StandardError; end
end

Redirect loops occur when servers return circular redirects. Clients must track visited URLs and limit redirect following.

require 'net/http'

class RedirectHandler
  MAX_REDIRECTS = 5
  
  def initialize(base_url)
    @base_url = base_url
  end

  def get_with_redirect(path, visited: Set.new, redirect_count: 0)
    if redirect_count >= MAX_REDIRECTS
      raise TooManyRedirects, "Exceeded maximum redirects (#{MAX_REDIRECTS})"
    end
    
    uri = URI("#{@base_url}#{path}")
    
    # Check for redirect loop
    if visited.include?(uri.to_s)
      raise RedirectLoop, "Redirect loop detected: #{uri}"
    end
    
    visited.add(uri.to_s)
    
    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = uri.scheme == 'https'
    
    request = Net::HTTP::Get.new(uri)
    response = http.request(request)
    
    case response.code.to_i
    when 200..299
      response
      
    when 300..399
      handle_redirect(response, visited, redirect_count)
      
    else
      response
    end
  end

  private

  def handle_redirect(response, visited, redirect_count)
    location = response['Location']
    
    unless location
      raise MissingLocation, "Redirect without Location header"
    end
    
    case response.code.to_i
    when 301, 308
      # Permanent redirect - cache the new location
      puts "Permanent redirect to #{location}"
      follow_redirect(location, visited, redirect_count)
      
    when 302, 303, 307
      # Temporary redirect
      puts "Temporary redirect to #{location}"
      follow_redirect(location, visited, redirect_count)
      
    when 304
      # Not modified - no redirect
      response
      
    else
      response
    end
  end

  def follow_redirect(location, visited, redirect_count)
    # Handle relative URLs
    new_uri = location.start_with?('http') ? location : "#{@base_url}#{location}"
    
    get_with_redirect(
      new_uri,
      visited: visited,
      redirect_count: redirect_count + 1
    )
  end

  class TooManyRedirects < StandardError; end
  class RedirectLoop < StandardError; end
  class MissingLocation < StandardError; end
end

Concurrent request handling prevents race conditions in state-changing operations. Using ETags with If-Match headers implements optimistic locking.

class ConcurrentUpdateHandler
  def update_with_lock(resource_id, updates)
    # Get current version
    current = fetch_resource(resource_id)
    etag = current[:etag]
    
    # Attempt update with version check
    response = send_update(resource_id, updates, etag)
    
    case response.code
    when '200', '204'
      # Success
      { status: :updated, data: parse_response(response) }
      
    when '412'
      # Precondition failed - resource changed
      handle_conflict(resource_id, updates, current)
      
    when '409'
      # Conflict - cannot update
      { status: :conflict, message: 'Resource conflict' }
      
    else
      { status: :error, code: response.code }
    end
  end

  private

  def fetch_resource(id)
    uri = URI("#{@base_url}/resources/#{id}")
    response = Net::HTTP.get_response(uri)
    
    {
      data: JSON.parse(response.body),
      etag: response['ETag']
    }
  end

  def send_update(id, updates, etag)
    uri = URI("#{@base_url}/resources/#{id}")
    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = true
    
    request = Net::HTTP::Put.new(uri)
    request['Content-Type'] = 'application/json'
    request['If-Match'] = etag
    request.body = JSON.generate(updates)
    
    http.request(request)
  end

  def handle_conflict(id, updates, current)
    # Fetch latest version
    latest = fetch_resource(id)
    
    # Merge changes
    merged = merge_updates(current[:data], latest[:data], updates)
    
    # Retry with new ETag
    response = send_update(id, merged, latest[:etag])
    
    if response.code == '412'
      # Still conflicted, fail
      { status: :conflict, message: 'Cannot resolve conflict' }
    else
      { status: :updated, data: parse_response(response) }
    end
  end

  def merge_updates(original, current, updates)
    # Three-way merge logic
    merged = current.dup
    
    updates.each do |key, value|
      # Only apply if field hasn't changed from original
      if current[key] == original[key]
        merged[key] = value
      end
    end
    
    merged
  end
end

Method not allowed handling returns 405 with an Allow header listing supported methods.

class MethodNotAllowedHandler < ApplicationController
  rescue_from ActionController::RoutingError, with: :method_not_allowed

  def method_not_allowed
    allowed_methods = determine_allowed_methods
    
    response.headers['Allow'] = allowed_methods.join(', ')
    
    render json: {
      error: 'Method Not Allowed',
      allowed_methods: allowed_methods
    }, status: :method_not_allowed
  end

  private

  def determine_allowed_methods
    # Inspect routes to find allowed methods for this path
    routes = Rails.application.routes.routes
    path = request.path
    
    matching_routes = routes.select { |r| r.path.match(path) }
    matching_routes.map { |r| r.verb }.compact.uniq
  end
end

Reference

HTTP Methods

Method Safe Idempotent Cacheable Request Body Response Body Common Use
GET Yes Yes Yes No Yes Retrieve resource
HEAD Yes Yes Yes No No Check resource existence
POST No No Yes* Yes Yes Create resource
PUT No Yes No Yes Optional Replace resource
PATCH No No No Yes Optional Modify resource
DELETE No Yes No Optional Optional Remove resource
OPTIONS Yes Yes No No Yes Get communication options
TRACE Yes Yes No No Yes Message loop-back test
CONNECT No No No Optional Yes Establish tunnel

*POST cacheable only with explicit cache controls

Common Status Codes

Code Name Category Meaning Response Body Common Use
200 OK Success Request succeeded Yes Successful GET, PUT, PATCH
201 Created Success Resource created Yes Successful POST
204 No Content Success Success without content No Successful DELETE, PUT
301 Moved Permanently Redirect Permanent redirect Optional Resource moved permanently
302 Found Redirect Temporary redirect Optional Temporary redirect
304 Not Modified Redirect Cached version valid No Conditional GET unchanged
400 Bad Request Client Error Malformed request Yes Invalid syntax
401 Unauthorized Client Error Authentication required Yes Missing credentials
403 Forbidden Client Error Access denied Yes Insufficient permissions
404 Not Found Client Error Resource not found Yes Missing resource
405 Method Not Allowed Client Error Method unsupported Yes Wrong HTTP method
409 Conflict Client Error Request conflicts Yes Duplicate resource
422 Unprocessable Entity Client Error Semantic errors Yes Validation failure
429 Too Many Requests Client Error Rate limit exceeded Yes Too many requests
500 Internal Server Error Server Error Server failure Yes Unhandled error
502 Bad Gateway Server Error Invalid upstream response Yes Proxy received invalid response
503 Service Unavailable Server Error Temporary unavailability Yes Server overloaded
504 Gateway Timeout Server Error Upstream timeout Yes Proxy timeout

Status Code Classes

Class Range Meaning Client Action
Informational 100-199 Request received, continuing Continue waiting
Success 200-299 Request successful Process response
Redirection 300-399 Further action needed Follow redirect
Client Error 400-499 Request error Fix and retry
Server Error 500-599 Server failure Retry with backoff

Method Properties

Property Methods Description
Safe GET, HEAD, OPTIONS, TRACE No server state modification
Idempotent GET, HEAD, PUT, DELETE, OPTIONS, TRACE Same result when repeated
Cacheable GET, HEAD, POST Response can be cached

Common Headers

Header Direction Methods Purpose Example
Content-Type Request/Response POST, PUT, PATCH Specify body format application/json
Content-Length Request/Response POST, PUT, PATCH Body size in bytes 1024
Location Response POST, 3xx Resource location /users/123
Allow Response OPTIONS, 405 Supported methods GET, POST, PUT
ETag Response GET, HEAD Resource version W/"abc123"
If-Match Request PUT, PATCH, DELETE Conditional update W/"abc123"
If-None-Match Request GET, HEAD Conditional request W/"abc123"
Last-Modified Response GET, HEAD Modification time Wed, 21 Oct 2025 07:28:00 GMT
If-Modified-Since Request GET, HEAD Conditional request Wed, 21 Oct 2025 07:28:00 GMT
Cache-Control Response GET, HEAD Caching directives max-age=3600, private
Retry-After Response 429, 503 Retry timing 60
Authorization Request All Authentication token Bearer abc123

Rails Status Code Symbols

Symbol Code Usage
:ok 200 render status: :ok
:created 201 render status: :created
:no_content 204 head :no_content
:moved_permanently 301 redirect_to url, status: :moved_permanently
:found 302 redirect_to url
:not_modified 304 head :not_modified
:bad_request 400 render status: :bad_request
:unauthorized 401 render status: :unauthorized
:forbidden 403 render status: :forbidden
:not_found 404 render status: :not_found
:method_not_allowed 405 render status: :method_not_allowed
:conflict 409 render status: :conflict
:unprocessable_entity 422 render status: :unprocessable_entity
:too_many_requests 429 render status: :too_many_requests
:internal_server_error 500 render status: :internal_server_error
:service_unavailable 503 render status: :service_unavailable

RESTful Route Conventions

Path Method Action Purpose Status on Success
/resources GET index List resources 200
/resources POST create Create resource 201
/resources/:id GET show Show resource 200
/resources/:id PUT update Replace resource 200 or 204
/resources/:id PATCH update Modify resource 200 or 204
/resources/:id DELETE destroy Delete resource 204 or 200

Net::HTTP Request Classes

Class Method Body Supported
Net::HTTP::Get GET No
Net::HTTP::Head HEAD No
Net::HTTP::Post POST Yes
Net::HTTP::Put PUT Yes
Net::HTTP::Patch PATCH Yes
Net::HTTP::Delete DELETE Optional
Net::HTTP::Options OPTIONS No

Decision Matrix: PUT vs PATCH

Scenario Method Reason
Replace entire resource PUT Complete replacement
Update specific fields PATCH Partial modification
Client has full resource state PUT Can provide complete representation
Client has only changed fields PATCH Send only changes
Idempotency required PUT PUT is idempotent
Minimize payload size PATCH Send only modifications

Error Response Format

# Validation error (422)
{
  error: 'Unprocessable Entity',
  errors: {
    email: ['is invalid', 'has already been taken'],
    name: ['is too short']
  }
}

# Not found (404)
{
  error: 'Not Found',
  message: 'Resource with id 123 not found'
}

# Server error (500)
{
  error: 'Internal Server Error',
  message: 'An unexpected error occurred'
}

# Rate limit (429)
{
  error: 'Too Many Requests',
  message: 'Rate limit exceeded',
  retry_after: 60
}