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