CrackedRuby CrackedRuby

Overview

HTTP (Hypertext Transfer Protocol) and HTTPS (HTTP Secure) form the foundation of data communication on the World Wide Web. HTTP defines how messages are formatted and transmitted between clients and servers, establishing a request-response protocol in the client-server computing model. A web browser acts as a client, while an application running on a server hosting a website acts as the server.

HTTP operates as an application layer protocol on top of TCP/IP. When a user enters a URL in their browser, the browser sends an HTTP request to the server, which processes the request and returns an HTTP response containing the requested resource or an error message. This stateless protocol treats each request as independent, with no inherent memory of previous requests.

HTTPS extends HTTP by adding a security layer through TLS (Transport Layer Security), previously known as SSL (Secure Sockets Layer). This encryption layer protects data in transit from eavesdropping, tampering, and message forgery. HTTPS has become the standard for web communication, with modern browsers marking non-HTTPS sites as "not secure."

require 'net/http'
require 'uri'

# Basic HTTP request
uri = URI('http://example.com/api/users')
response = Net::HTTP.get_response(uri)
puts response.code    # => "200"
puts response.body    # => Response content

# HTTPS request with verification
uri = URI('https://api.example.com/data')
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
response = http.get(uri.path)

The protocol operates on port 80 for HTTP and port 443 for HTTPS by default. Understanding both protocols remains essential for web development, API integration, and any application requiring network communication.

Key Principles

HTTP follows a request-response model where clients initiate communication and servers respond. Each HTTP message consists of a start line, headers, an empty line, and an optional body. The start line differs between requests and responses: requests contain a method, path, and HTTP version, while responses contain the HTTP version, status code, and status text.

HTTP methods define the action to perform on a resource. GET retrieves data without side effects. POST submits data that may cause server-side changes or side effects. PUT replaces a resource entirely with new data. PATCH applies partial modifications to a resource. DELETE removes a resource. HEAD retrieves headers without the response body, useful for checking resource existence or metadata. OPTIONS describes communication options for the target resource.

Status codes communicate the result of a request. The 1xx range indicates informational responses. 2xx codes signal successful requests, with 200 OK being the standard success response. 3xx codes handle redirection, requiring additional client action. 4xx codes indicate client errors, such as 404 Not Found for missing resources or 401 Unauthorized for authentication failures. 5xx codes represent server errors, like 500 Internal Server Error for unexpected server conditions.

require 'net/http'

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

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

response = http.request(request)

case response
when Net::HTTPSuccess
  puts "Success: #{response.body}"
when Net::HTTPRedirection
  puts "Redirected to: #{response['location']}"
when Net::HTTPClientError
  puts "Client error: #{response.code}"
when Net::HTTPServerError
  puts "Server error: #{response.code}"
end

Headers provide metadata about the request or response. Content-Type specifies the media type of the body. Content-Length indicates the body size in bytes. Accept tells the server what content types the client can process. Authorization carries credentials for authentication. Cache-Control directs caching mechanisms. Set-Cookie sends cookies from server to client, while Cookie sends stored cookies from client to server.

HTTP operates as a stateless protocol, meaning each request contains all information needed to understand and process it. Servers do not retain client state between requests. Applications achieve statefulness through mechanisms like cookies, session tokens, or URL parameters. This statelessness enables horizontal scaling since any server can handle any request without sharing state with other servers.

Connection management affects performance and resource usage. HTTP/1.0 opened a new TCP connection for each request-response pair. HTTP/1.1 introduced persistent connections that remain open for multiple requests, reducing overhead. The Connection header controls this behavior: "close" terminates the connection after the response, while "keep-alive" maintains it. HTTP/2 multiplexes multiple requests over a single connection, further improving efficiency.

HTTPS wraps HTTP communication in TLS, providing three security properties: confidentiality through encryption, integrity through message authentication codes, and authentication through certificate verification. The TLS handshake establishes a secure connection by negotiating encryption algorithms, exchanging keys, and verifying the server's certificate. Modern TLS uses asymmetric encryption during the handshake and symmetric encryption for data transfer, balancing security and performance.

Ruby Implementation

Ruby provides multiple libraries for HTTP communication, each with different design philosophies and use cases. The standard library includes Net::HTTP, which offers direct control over HTTP mechanics. Third-party gems like HTTParty, Faraday, and RestClient provide higher-level abstractions that simplify common tasks.

Net::HTTP ships with Ruby and requires no external dependencies. It exposes low-level HTTP operations, making it suitable when precise control over requests is needed. Creating requests involves instantiating request objects, setting headers, and providing bodies. The library handles connection management, redirects (when configured), and response parsing.

require 'net/http'
require 'json'

class APIClient
  def initialize(base_url)
    @uri = URI(base_url)
    @http = Net::HTTP.new(@uri.host, @uri.port)
    @http.use_ssl = @uri.scheme == 'https'
    @http.read_timeout = 10
    @http.open_timeout = 5
  end

  def get(path, headers = {})
    request = Net::HTTP::Get.new(path)
    headers.each { |key, value| request[key] = value }
    execute_request(request)
  end

  def post(path, body, headers = {})
    request = Net::HTTP::Post.new(path)
    request.body = body.to_json
    request['Content-Type'] = 'application/json'
    headers.each { |key, value| request[key] = value }
    execute_request(request)
  end

  private

  def execute_request(request)
    response = @http.request(request)
    {
      status: response.code.to_i,
      headers: response.to_hash,
      body: parse_body(response)
    }
  end

  def parse_body(response)
    return nil if response.body.nil? || response.body.empty?
    JSON.parse(response.body)
  rescue JSON::ParserError
    response.body
  end
end

# Usage
client = APIClient.new('https://api.example.com')
result = client.get('/users/123', { 'Authorization' => 'Bearer token' })
puts result[:status]  # => 200

HTTParty simplifies HTTP requests with a DSL that reduces boilerplate. It automatically parses JSON and XML responses, follows redirects, and provides convenient methods for common operations. HTTParty works well for straightforward API consumption where low-level control is unnecessary.

require 'httparty'

class GithubAPI
  include HTTParty
  base_uri 'https://api.github.com'
  headers 'User-Agent' => 'Ruby HTTParty'

  def self.get_user(username)
    get("/users/#{username}")
  end

  def self.create_issue(repo, title, body, token)
    post("/repos/#{repo}/issues",
      body: { title: title, body: body }.to_json,
      headers: {
        'Content-Type' => 'application/json',
        'Authorization' => "token #{token}"
      }
    )
  end
end

# Usage
user = GithubAPI.get_user('octocat')
puts user['name']  # => "The Octocat"

response = GithubAPI.create_issue(
  'owner/repo',
  'Bug Report',
  'Description of the bug',
  'github_token_here'
)
puts response.code  # => 201

Faraday provides middleware-based architecture, allowing developers to build custom request/response processing pipelines. This design enables adding logging, caching, retry logic, or authentication handling as modular components. Faraday supports multiple adapter backends, making it adapter-agnostic and testable.

require 'faraday'
require 'faraday/retry'

# Connection with middleware stack
conn = Faraday.new(url: 'https://api.example.com') do |f|
  f.request :json  # Encode request bodies as JSON
  f.request :retry, max: 3, interval: 0.5
  f.response :json  # Decode response bodies as JSON
  f.response :logger, Logger.new($stdout)
  f.adapter Faraday.default_adapter
end

# Request with custom headers
response = conn.get('/users') do |req|
  req.headers['Authorization'] = 'Bearer token'
  req.params['limit'] = 10
end

puts response.status  # => 200
puts response.body    # => Parsed JSON object

# POST with automatic JSON encoding
response = conn.post('/users') do |req|
  req.body = { name: 'Bob', email: 'bob@example.com' }
end

WebSocket communication requires different handling since it establishes persistent bidirectional connections. The websocket-driver gem provides WebSocket protocol implementation for Ruby. Rails ActionCable builds on WebSocket for real-time features like chat or live updates.

require 'faye/websocket'
require 'eventmachine'

EM.run do
  ws = Faye::WebSocket::Client.new('wss://echo.websocket.org/')

  ws.on :open do |event|
    puts 'Connection opened'
    ws.send('Hello, server!')
  end

  ws.on :message do |event|
    puts "Received: #{event.data}"
    ws.close
  end

  ws.on :close do |event|
    puts "Connection closed: #{event.code}"
    EM.stop
  end
end

Security Implications

HTTPS encrypts data in transit, preventing interception and tampering. Without HTTPS, attackers on the network path can read sensitive information like passwords, session tokens, or personal data. Man-in-the-middle attacks become trivial on unencrypted connections, allowing attackers to inject malicious content or steal credentials.

Certificate verification ensures the server's identity matches the domain requested. The TLS handshake validates the server's certificate against trusted Certificate Authorities. Ruby's OpenSSL bindings provide verification modes: VERIFY_NONE disables verification (dangerous), VERIFY_PEER validates certificates (required for production), and custom verification callbacks enable additional checks.

require 'net/http'
require 'openssl'

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

# Secure configuration
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
http.cert_store = OpenSSL::X509::Store.new
http.cert_store.set_default_paths  # Use system CA certificates

# Optional: Pin specific certificate
expected_cert = OpenSSL::X509::Certificate.new(File.read('expected_cert.pem'))
http.verify_callback = proc do |verify_ok, store_context|
  if verify_ok
    cert = store_context.current_cert
    verify_ok = (cert.to_der == expected_cert.to_der)
  end
  verify_ok
end

response = http.get(uri.path)

Never disable SSL verification in production code. Setting verify_mode to VERIFY_NONE exposes applications to man-in-the-middle attacks. Developers sometimes disable verification during development to work around certificate issues, but this creates security holes if the code reaches production. Use proper certificates even in development environments, or use separate configuration for different environments.

Authentication mechanisms secure API access. Basic authentication encodes credentials in headers, but requires HTTPS since Base64 encoding provides no encryption. Bearer tokens, typically JWT (JSON Web Tokens), carry authentication information and authorization claims. API keys identify clients but should be treated as secrets and transmitted only over HTTPS. OAuth 2.0 provides delegated authorization without sharing credentials.

require 'httparty'
require 'base64'

class SecureAPI
  include HTTParty
  base_uri 'https://api.example.com'

  # Basic authentication (requires HTTPS)
  def self.basic_auth_request(username, password, path)
    basic_auth(username, password)
    get(path)
  end

  # Bearer token authentication
  def self.token_request(token, path)
    get(path, headers: { 'Authorization' => "Bearer #{token}" })
  end

  # API key in header
  def self.api_key_request(api_key, path)
    get(path, headers: { 'X-API-Key' => api_key })
  end
end

# Usage - all require HTTPS
response = SecureAPI.basic_auth_request('user', 'pass', '/protected')
response = SecureAPI.token_request('jwt_token', '/data')
response = SecureAPI.api_key_request('key_123', '/resources')

Cross-Site Request Forgery (CSRF) exploits trust between a site and a user's browser. Attackers trick authenticated users into making unwanted requests. Rails protects against CSRF by requiring authenticity tokens in forms. APIs using token-based authentication are less vulnerable since tokens are not automatically sent by browsers like cookies are.

Content Security Policy (CSP) headers mitigate XSS attacks by restricting resource loading. HTTP Strict Transport Security (HSTS) forces browsers to use HTTPS for all future requests to a domain. X-Frame-Options prevents clickjacking by controlling whether pages can be embedded in frames.

Sensitive data should never appear in URLs since URLs are logged by browsers, proxies, and servers. Query parameters containing passwords or tokens create security vulnerabilities. Use POST requests with encrypted bodies for sensitive data transmission.

Rate limiting protects APIs from abuse and denial-of-service attacks. Servers return 429 Too Many Requests when clients exceed limits. The Retry-After header indicates when clients can retry. Implementing exponential backoff in clients respects server resources and improves reliability.

require 'faraday'

class RateLimitedClient
  def initialize(base_url)
    @conn = Faraday.new(url: base_url)
    @max_retries = 3
  end

  def get_with_retry(path)
    retries = 0
    begin
      response = @conn.get(path)
      
      if response.status == 429
        retry_after = response.headers['retry-after']&.to_i || (2 ** retries)
        raise RateLimitError.new(retry_after)
      end
      
      response
    rescue RateLimitError => e
      retries += 1
      if retries <= @max_retries
        sleep(e.retry_after)
        retry
      else
        raise "Max retries exceeded"
      end
    end
  end
end

class RateLimitError < StandardError
  attr_reader :retry_after
  
  def initialize(retry_after)
    @retry_after = retry_after
    super("Rate limit exceeded, retry after #{retry_after} seconds")
  end
end

Practical Examples

Building a REST API client demonstrates HTTP fundamentals in practice. This example shows request handling, error management, and response processing for a typical API integration.

require 'net/http'
require 'json'

class WeatherAPIClient
  API_BASE = 'https://api.weather.com/v1'
  
  def initialize(api_key)
    @api_key = api_key
    @uri = URI(API_BASE)
    @http = Net::HTTP.new(@uri.host, @uri.port)
    @http.use_ssl = true
    @http.read_timeout = 10
  end

  def current_weather(city)
    path = "/current?city=#{URI.encode_www_form_component(city)}"
    request = build_request(Net::HTTP::Get, path)
    
    response = @http.request(request)
    handle_response(response)
  end

  def forecast(city, days: 7)
    path = "/forecast"
    params = {
      city: city,
      days: days,
      units: 'metric'
    }
    path += "?#{URI.encode_www_form(params)}"
    
    request = build_request(Net::HTTP::Get, path)
    response = @http.request(request)
    handle_response(response)
  end

  private

  def build_request(request_class, path)
    request = request_class.new(path)
    request['Authorization'] = "Bearer #{@api_key}"
    request['Accept'] = 'application/json'
    request['User-Agent'] = 'WeatherClient/1.0'
    request
  end

  def handle_response(response)
    case response
    when Net::HTTPSuccess
      JSON.parse(response.body)
    when Net::HTTPUnauthorized
      raise AuthenticationError, "Invalid API key"
    when Net::HTTPNotFound
      raise NotFoundError, "City not found"
    when Net::HTTPTooManyRequests
      raise RateLimitError, "Rate limit exceeded"
    when Net::HTTPServerError
      raise ServerError, "Server error: #{response.code}"
    else
      raise APIError, "Unexpected response: #{response.code}"
    end
  end
end

# Custom exceptions
class APIError < StandardError; end
class AuthenticationError < APIError; end
class NotFoundError < APIError; end
class RateLimitError < APIError; end
class ServerError < APIError; end

# Usage
client = WeatherAPIClient.new('your_api_key')

begin
  weather = client.current_weather('London')
  puts "Temperature: #{weather['temperature']}°C"
  puts "Condition: #{weather['condition']}"
  
  forecast = client.forecast('London', days: 5)
  forecast['daily'].each do |day|
    puts "#{day['date']}: #{day['high']}°C / #{day['low']}°C"
  end
rescue AuthenticationError => e
  puts "Authentication failed: #{e.message}"
rescue NotFoundError => e
  puts "City not found: #{e.message}"
rescue RateLimitError => e
  puts "Rate limited: #{e.message}"
end

File uploads require multipart/form-data encoding to send binary data alongside form fields. This example demonstrates uploading files with metadata.

require 'net/http'
require 'uri'

class FileUploader
  def initialize(endpoint)
    @uri = URI(endpoint)
    @http = Net::HTTP.new(@uri.host, @uri.port)
    @http.use_ssl = @uri.scheme == 'https'
  end

  def upload_file(file_path, metadata = {})
    boundary = "----WebKitFormBoundary#{rand(10**16)}"
    
    request = Net::HTTP::Post.new(@uri.path)
    request['Content-Type'] = "multipart/form-data; boundary=#{boundary}"
    request.body = build_multipart_body(file_path, metadata, boundary)
    
    response = @http.request(request)
    
    if response.is_a?(Net::HTTPSuccess)
      JSON.parse(response.body)
    else
      raise "Upload failed: #{response.code} #{response.message}"
    end
  end

  private

  def build_multipart_body(file_path, metadata, boundary)
    parts = []
    
    # Add metadata fields
    metadata.each do |key, value|
      parts << "--#{boundary}\r\n"
      parts << "Content-Disposition: form-data; name=\"#{key}\"\r\n\r\n"
      parts << "#{value}\r\n"
    end
    
    # Add file
    file_name = File.basename(file_path)
    file_content = File.read(file_path)
    
    parts << "--#{boundary}\r\n"
    parts << "Content-Disposition: form-data; name=\"file\"; filename=\"#{file_name}\"\r\n"
    parts << "Content-Type: application/octet-stream\r\n\r\n"
    parts << file_content
    parts << "\r\n--#{boundary}--\r\n"
    
    parts.join
  end
end

# Usage
uploader = FileUploader.new('https://api.example.com/upload')
result = uploader.upload_file(
  'document.pdf',
  {
    'title' => 'Important Document',
    'category' => 'reports',
    'user_id' => '12345'
  }
)
puts "Uploaded successfully: #{result['file_id']}"

Implementing retry logic with exponential backoff improves reliability when dealing with transient failures.

require 'faraday'

class ResilientHTTPClient
  MAX_RETRIES = 3
  INITIAL_BACKOFF = 1
  MAX_BACKOFF = 30
  
  def initialize(base_url)
    @conn = Faraday.new(url: base_url) do |f|
      f.response :json
      f.adapter Faraday.default_adapter
    end
  end

  def get_with_retry(path, headers: {})
    execute_with_retry do
      @conn.get(path, nil, headers)
    end
  end

  def post_with_retry(path, body, headers: {})
    execute_with_retry do
      @conn.post(path, body, headers)
    end
  end

  private

  def execute_with_retry
    retries = 0
    backoff = INITIAL_BACKOFF
    
    begin
      response = yield
      
      # Retry on server errors or specific status codes
      if should_retry?(response.status, retries)
        raise RetryableError.new(response.status)
      end
      
      response
      
    rescue Faraday::Error, RetryableError => e
      retries += 1
      
      if retries <= MAX_RETRIES
        sleep(backoff)
        backoff = [backoff * 2, MAX_BACKOFF].min  # Exponential with cap
        retry
      else
        raise "Max retries (#{MAX_RETRIES}) exceeded: #{e.message}"
      end
    end
  end

  def should_retry?(status, retries)
    return false if retries >= MAX_RETRIES
    
    # Retry on server errors or specific client errors
    status >= 500 || status == 429 || status == 408
  end
end

class RetryableError < StandardError
  attr_reader :status
  
  def initialize(status)
    @status = status
    super("Retryable HTTP error: #{status}")
  end
end

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

begin
  response = client.get_with_retry('/users', headers: { 'Authorization' => 'Bearer token' })
  puts "Retrieved #{response.body['users'].length} users"
rescue => e
  puts "Request failed after retries: #{e.message}"
end

Performance Considerations

HTTP/1.1 persistent connections reduce overhead by reusing TCP connections for multiple requests. Opening a new TCP connection for each request incurs three-way handshake latency plus TLS handshake for HTTPS. Persistent connections amortize this cost across many requests. The Connection: keep-alive header maintains connections, though HTTP/1.1 assumes keep-alive by default.

require 'net/http'

# Inefficient: Creates new connection per request
def fetch_multiple_inefficient(urls)
  urls.map do |url|
    uri = URI(url)
    Net::HTTP.get(uri)
  end
end

# Efficient: Reuses connection
def fetch_multiple_efficient(base_url, paths)
  uri = URI(base_url)
  Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
    paths.map do |path|
      response = http.get(path)
      response.body
    end
  end
end

# Reuses connection for 100 requests to same host
results = fetch_multiple_efficient('https://api.example.com', 
  (1..100).map { |i| "/users/#{i}" }
)

Request pipelining sends multiple requests without waiting for responses, though few HTTP/1.1 implementations support it safely. HTTP/2 provides true multiplexing, allowing multiple request-response pairs simultaneously over one connection. Ruby's net-http2 gem enables HTTP/2 support.

Compression reduces transfer size significantly. The Accept-Encoding header tells servers which compression methods the client supports. Servers respond with Content-Encoding indicating the compression used. Gzip typically reduces JSON payloads by 70-90%. Ruby's Net::HTTP automatically handles decompression when servers send compressed responses.

require 'net/http'
require 'zlib'

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

request = Net::HTTP::Get.new(uri.path)
request['Accept-Encoding'] = 'gzip, deflate'

response = http.request(request)

# Net::HTTP automatically decompresses gzip responses
data = JSON.parse(response.body)
puts "Received #{data.size} items"
puts "Original size: #{response['content-length']}"
puts "Encoding used: #{response['content-encoding']}"

Caching eliminates redundant requests. Cache-Control headers specify caching policies. max-age defines how long responses remain fresh. no-cache forces revalidation with the server. no-store prevents caching entirely. ETags enable conditional requests: clients send If-None-Match headers, and servers respond with 304 Not Modified if content hasn't changed, avoiding full response transmission.

require 'faraday'
require 'faraday_middleware'

# Client with caching middleware
conn = Faraday.new(url: 'https://api.example.com') do |f|
  f.response :caching do
    ActiveSupport::Cache::MemoryStore.new(size: 64.megabytes)
  end
  f.response :json
  f.adapter Faraday.default_adapter
end

# First request: Cache miss, full response
response = conn.get('/users')  
puts response.body  # Full data

# Second request: Cache hit, no network request
response = conn.get('/users')  
puts response.body  # Same data, instant response

# Conditional request implementation
class ConditionalClient
  def initialize(base_url)
    @conn = Faraday.new(url: base_url)
    @etag_cache = {}
  end

  def get_conditional(path)
    headers = {}
    headers['If-None-Match'] = @etag_cache[path] if @etag_cache[path]
    
    response = @conn.get(path, nil, headers)
    
    if response.status == 304
      puts "Not modified, using cached data"
      @cached_data[path]
    else
      @etag_cache[path] = response.headers['etag']
      @cached_data[path] = response.body
      response.body
    end
  end
end

Connection pooling manages multiple simultaneous requests efficiently. Creating too many connections overwhelms servers and exhausts client resources. Connection pools limit concurrent connections while queueing additional requests. Typhoeus and parallel_http gems provide connection pooling for concurrent requests.

require 'typhoeus'

# Concurrent requests with connection pooling
hydra = Typhoeus::Hydra.new(max_concurrency: 10)

requests = (1..100).map do |i|
  request = Typhoeus::Request.new("https://api.example.com/users/#{i}")
  hydra.queue(request)
  request
end

hydra.run  # Executes queued requests with max 10 concurrent

requests.each do |request|
  if request.response.success?
    puts "User #{request.url}: #{request.response.body}"
  else
    puts "Failed: #{request.response.code}"
  end
end

Timeout configuration prevents hanging requests from blocking applications. Connect timeouts limit TCP connection establishment time. Read timeouts limit waiting for response data. Write timeouts limit sending request data. Setting appropriate timeouts based on expected response times maintains application responsiveness.

Response streaming processes large responses incrementally rather than loading entire responses into memory. This approach reduces memory usage and decreases time to first byte for large files or continuous data streams.

require 'net/http'

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

# Stream large response
http.request_get(uri.path) do |response|
  if response.is_a?(Net::HTTPSuccess)
    File.open('output.json', 'wb') do |file|
      response.read_body do |chunk|
        file.write(chunk)
        # Process chunk immediately
        print '.'
      end
    end
    puts "\nDownload complete"
  end
end

Integration & Interoperability

REST APIs follow architectural constraints for building scalable web services. Resources are identified by URIs. HTTP methods map to CRUD operations: GET retrieves, POST creates, PUT updates, DELETE removes. Responses include appropriate status codes and content negotiation via Accept headers allows clients to specify desired formats.

require 'sinatra'
require 'json'

# Simple REST API server
class UserAPI < Sinatra::Base
  configure do
    set :users, []
  end

  get '/users' do
    content_type :json
    settings.users.to_json
  end

  get '/users/:id' do
    content_type :json
    user = settings.users.find { |u| u[:id] == params[:id].to_i }
    halt 404, { error: 'User not found' }.to_json unless user
    user.to_json
  end

  post '/users' do
    content_type :json
    data = JSON.parse(request.body.read)
    user = { id: settings.users.length + 1, name: data['name'], email: data['email'] }
    settings.users << user
    status 201
    user.to_json
  end

  put '/users/:id' do
    content_type :json
    user = settings.users.find { |u| u[:id] == params[:id].to_i }
    halt 404, { error: 'User not found' }.to_json unless user
    
    data = JSON.parse(request.body.read)
    user.merge!(name: data['name'], email: data['email'])
    user.to_json
  end

  delete '/users/:id' do
    user = settings.users.find { |u| u[:id] == params[:id].to_i }
    halt 404 unless user
    settings.users.delete(user)
    status 204
  end
end

GraphQL provides an alternative to REST, allowing clients to request specific data structures. A single endpoint serves all queries. Clients specify exactly what data they need, reducing over-fetching and under-fetching problems common with REST.

Webhooks enable servers to push notifications to clients instead of clients polling for updates. The receiving application exposes an HTTP endpoint. The sending application makes HTTP POST requests to that endpoint when events occur. Webhook signatures verify authenticity using HMAC.

require 'sinatra'
require 'json'
require 'openssl'

class WebhookReceiver < Sinatra::Base
  WEBHOOK_SECRET = ENV['WEBHOOK_SECRET']

  post '/webhook' do
    payload = request.body.read
    signature = request.env['HTTP_X_SIGNATURE']

    unless verify_signature(payload, signature)
      halt 401, 'Invalid signature'
    end

    event = JSON.parse(payload)
    process_event(event)
    
    status 200
  end

  private

  def verify_signature(payload, signature)
    expected = OpenSSL::HMAC.hexdigest('SHA256', WEBHOOK_SECRET, payload)
    Rack::Utils.secure_compare(expected, signature)
  end

  def process_event(event)
    case event['type']
    when 'user.created'
      puts "New user: #{event['data']['name']}"
    when 'order.completed'
      puts "Order #{event['data']['order_id']} completed"
    end
  end
end

Server-Sent Events (SSE) stream updates from server to client over a single HTTP connection. The server sends data events as text/event-stream. Browsers automatically reconnect if connections drop. SSE works over standard HTTP, unlike WebSockets which require protocol upgrade.

CORS (Cross-Origin Resource Sharing) controls which domains can access resources. Browsers enforce same-origin policy, blocking requests from different origins unless CORS headers permit them. Preflight requests using OPTIONS verify permissions before sending actual requests. Access-Control-Allow-Origin specifies permitted origins. Access-Control-Allow-Methods lists allowed HTTP methods.

require 'sinatra'

class CORSEnabled < Sinatra::Base
  before do
    headers['Access-Control-Allow-Origin'] = 'https://example.com'
    headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS'
    headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'
    headers['Access-Control-Max-Age'] = '3600'
  end

  options '*' do
    200
  end

  get '/api/data' do
    content_type :json
    { data: 'This is CORS-enabled' }.to_json
  end
end

API versioning maintains backward compatibility while evolving interfaces. URI versioning includes version in the path (/v1/users). Header versioning uses custom headers (Accept: application/vnd.api+json;version=1). Content negotiation uses Accept headers with vendor-specific media types.

require 'sinatra'

class VersionedAPI < Sinatra::Base
  # URI versioning
  get '/v1/users' do
    { version: 1, users: get_users_v1 }.to_json
  end

  get '/v2/users' do
    { version: 2, users: get_users_v2 }.to_json
  end

  # Header versioning
  get '/users' do
    version = request.env['HTTP_API_VERSION'] || '1'
    case version
    when '1'
      { users: get_users_v1 }.to_json
    when '2'
      { users: get_users_v2 }.to_json
    else
      halt 400, { error: 'Unsupported API version' }.to_json
    end
  end

  private

  def get_users_v1
    [{ id: 1, name: 'Alice' }]
  end

  def get_users_v2
    [{ id: 1, full_name: 'Alice Smith', email: 'alice@example.com' }]
  end
end

Reference

HTTP Methods

Method Purpose Request Body Response Body Idempotent Safe
GET Retrieve resource No Yes Yes Yes
POST Create resource Yes Yes No No
PUT Replace resource Yes Yes Yes No
PATCH Modify resource Yes Yes No No
DELETE Remove resource No Optional Yes No
HEAD Get headers only No No Yes Yes
OPTIONS Describe options No Yes Yes Yes

Common Status Codes

Code Meaning Use Case
200 OK Successful GET, PUT, PATCH
201 Created Successful POST
204 No Content Successful DELETE
301 Moved Permanently Resource relocated permanently
302 Found Temporary redirect
304 Not Modified Cached resource still valid
400 Bad Request Invalid request syntax
401 Unauthorized Authentication required
403 Forbidden Authenticated but not authorized
404 Not Found Resource does not exist
405 Method Not Allowed HTTP method not supported
409 Conflict Request conflicts with current state
429 Too Many Requests Rate limit exceeded
500 Internal Server Error Generic server error
502 Bad Gateway Invalid response from upstream
503 Service Unavailable Server temporarily unavailable
504 Gateway Timeout Upstream timeout

Request Headers

Header Purpose Example
Accept Acceptable response formats application/json
Accept-Encoding Acceptable compression gzip, deflate
Authorization Authentication credentials Bearer token123
Content-Type Request body format application/json
Content-Length Request body size 1234
Cookie Stored cookies session=abc123
Host Target host api.example.com
User-Agent Client identification Ruby/3.1
If-None-Match Conditional request W/"abc123"
If-Modified-Since Conditional request Wed, 21 Oct 2024 07:28:00 GMT

Response Headers

Header Purpose Example
Content-Type Response body format application/json; charset=utf-8
Content-Length Response body size 2048
Content-Encoding Compression used gzip
Cache-Control Caching directives max-age=3600, private
ETag Resource version identifier W/"abc123"
Expires Expiration date Thu, 01 Dec 2024 16:00:00 GMT
Last-Modified Last modification date Wed, 21 Oct 2024 07:28:00 GMT
Location Redirect or created resource URI /users/123
Set-Cookie Cookie to store session=xyz; HttpOnly; Secure
Access-Control-Allow-Origin CORS permitted origin https://example.com
Retry-After Rate limit retry time 60

Ruby HTTP Libraries Comparison

Library Approach Use Case Pros Cons
Net::HTTP Low-level Fine-grained control Standard library, no dependencies Verbose, manual parsing
HTTParty High-level DSL Simple API consumption Easy syntax, auto-parsing Less flexible
Faraday Middleware Modular architecture Highly customizable, testable More setup required
RestClient Simple wrapper Quick integration Concise, easy to use Less maintained
Typhoeus Concurrent Parallel requests Fast, concurrent libcurl dependency

TLS/SSL Configuration

Setting Values Recommendation
use_ssl true, false Always true for HTTPS
verify_mode VERIFY_PEER, VERIFY_NONE Always VERIFY_PEER in production
cert_store X509::Store Use system certificates
ssl_version :TLSv1_2, :TLSv1_3 Prefer TLSv1_3, minimum TLSv1_2
ciphers Cipher suite list Use modern, secure ciphers
verify_hostname true, false Always true

Timeout Settings

Timeout Default Recommended Purpose
open_timeout 60s 5-10s TCP connection establishment
read_timeout 60s 10-30s Reading response data
write_timeout 60s 10-30s Sending request data
ssl_timeout 60s 10s SSL/TLS handshake
keep_alive_timeout 2s 2-5s Keep-alive probe interval

Cache-Control Directives

Directive Effect Use Case
public Cacheable by any cache Static resources
private Cacheable by browser only User-specific data
no-cache Revalidate before use Fresh data required
no-store Never cache Sensitive information
max-age Cache duration in seconds Control freshness
must-revalidate Strict expiration Prevent stale data
immutable Never revalidate Static assets with versioned URLs

Ruby HTTP Client Examples

# Net::HTTP basic request
require 'net/http'
uri = URI('https://api.example.com/users')
response = Net::HTTP.get_response(uri)

# HTTParty class-level request
require 'httparty'
response = HTTParty.get('https://api.example.com/users')

# Faraday with middleware
require 'faraday'
conn = Faraday.new(url: 'https://api.example.com') do |f|
  f.request :json
  f.response :json
  f.adapter Faraday.default_adapter
end
response = conn.get('/users')

# Setting headers
request = Net::HTTP::Get.new(uri)
request['Authorization'] = 'Bearer token'
request['Accept'] = 'application/json'

# POST with JSON body
require 'json'
request = Net::HTTP::Post.new(uri)
request['Content-Type'] = 'application/json'
request.body = { name: 'Alice' }.to_json