Overview
CGI::Cookie provides Ruby's interface for creating, parsing, and manipulating HTTP cookies in web applications. The class handles cookie serialization, parsing from HTTP headers, and generating proper Set-Cookie header values according to RFC specifications. Ruby implements cookies as objects containing name-value pairs along with attributes like expiration dates, domains, paths, and security flags.
The CGI::Cookie class integrates with Ruby's CGI library to manage stateful information between HTTP requests. When a browser sends cookies through the Cookie header, CGI::Cookie parses the header string into Ruby objects. When sending cookies to browsers, the class generates properly formatted Set-Cookie headers with all necessary attributes and encoding.
require 'cgi'
# Create a new cookie
cookie = CGI::Cookie.new('user_preference', 'dark_theme')
cookie.expires = Time.now + 86400 # 24 hours
cookie.path = '/'
cookie.secure = true
# Generate Set-Cookie header
puts cookie.to_s
# => user_preference=dark_theme; expires=Sun, 31 Aug 2025 12:00:00 GMT; path=/; secure
CGI::Cookie handles multiple values per cookie name, automatic URL encoding of cookie values, and proper handling of special characters. The class maintains separate arrays for cookie names and values, allowing complex data structures while remaining compliant with HTTP cookie specifications.
# Cookie with multiple values
multi_cookie = CGI::Cookie.new('colors', ['red', 'blue', 'green'])
puts multi_cookie.to_s
# => colors=red&blue&green
# Cookie with special characters (automatically encoded)
special_cookie = CGI::Cookie.new('message', 'Hello, World!')
puts special_cookie.to_s
# => message=Hello%2C+World%21
The cookie implementation supports all standard cookie attributes including HttpOnly for XSS protection, SameSite for CSRF protection, and domain restrictions for cross-domain cookie policies. CGI::Cookie automatically handles the complexity of cookie attribute formatting and browser compatibility requirements.
Basic Usage
Creating cookies requires specifying a name and one or more values. The CGI::Cookie constructor accepts various parameter formats, from simple string pairs to complex hash configurations with all cookie attributes.
require 'cgi'
# Simple cookie creation
session_cookie = CGI::Cookie.new('session_id', '3f8a9b2c1d4e5f6')
puts session_cookie.to_s
# => session_id=3f8a9b2c1d4e5f6
# Cookie with expiration
persistent_cookie = CGI::Cookie.new(
'name' => 'user_token',
'value' => 'abc123def456',
'expires' => Time.now + (30 * 86400) # 30 days
)
Cookie parsing from HTTP headers occurs automatically when processing incoming requests. The CGI library extracts cookie data from the HTTP_COOKIE environment variable and creates CGI::Cookie objects for each cookie sent by the browser.
# Simulate parsing cookies from HTTP header
ENV['HTTP_COOKIE'] = 'user_id=12345; theme=dark; lang=en'
cgi = CGI.new
# Access parsed cookies
user_id = cgi.cookies['user_id']
puts user_id[0] # => "12345"
theme_cookie = cgi.cookies['theme']
puts theme_cookie.value # => ["dark"]
Setting multiple values in a single cookie allows storing related data together. Each value gets URL-encoded automatically, and the cookie combines values using ampersand separators in the final header output.
# Multiple values in one cookie
preferences = CGI::Cookie.new('prefs', ['notifications_on', 'dark_theme', 'compact_view'])
puts preferences.to_s
# => prefs=notifications_on&dark_theme&compact_view
# Accessing individual values
preferences.value.each { |pref| puts pref }
# => notifications_on
# => dark_theme
# => compact_view
Cookie attributes control browser behavior and security properties. Path restrictions limit cookie scope to specific URL prefixes, while domain attributes enable cross-subdomain cookie sharing. The secure flag restricts transmission to HTTPS connections only.
# Comprehensive cookie configuration
secure_cookie = CGI::Cookie.new(
'name' => 'auth_token',
'value' => 'eyJ0eXAiOiJKV1QiLCJhbGci',
'path' => '/admin',
'domain' => '.example.com',
'expires' => Time.now + 3600, # 1 hour
'secure' => true,
'httponly' => true
)
puts secure_cookie.to_s
# => auth_token=eyJ0eXAiOiJKV1QiLCJhbGci; expires=Sat, 30 Aug 2025 13:00:00 GMT; domain=.example.com; path=/admin; secure; HttpOnly
Error Handling & Debugging
Cookie validation failures occur when cookie names contain invalid characters or when cookie values exceed browser size limitations. CGI::Cookie raises ArgumentError exceptions for malformed cookie names but silently truncates or encodes problematic values.
require 'cgi'
# Invalid cookie names cause exceptions
begin
invalid_cookie = CGI::Cookie.new('user name', 'john_doe') # Space in name
rescue ArgumentError => e
puts "Cookie name error: #{e.message}"
# Fix with valid name
valid_cookie = CGI::Cookie.new('user_name', 'john_doe')
end
# Cookie value size validation
large_value = 'x' * 5000 # Exceeds typical 4KB browser limit
size_cookie = CGI::Cookie.new('large_data', large_value)
puts "Cookie size: #{size_cookie.to_s.length} bytes"
# Check if size exceeds browser limits
if size_cookie.to_s.length > 4096
puts "Warning: Cookie exceeds typical browser limits"
end
Encoding issues arise when cookie values contain non-ASCII characters or reserved URL characters. CGI::Cookie automatically URL-encodes values, but problems occur when manually constructing cookie headers or when receiving malformed cookies from browsers.
# Handling encoding issues
unicode_value = "José's café" # Contains non-ASCII characters
encoded_cookie = CGI::Cookie.new('username', unicode_value)
puts encoded_cookie.to_s
# => username=Jos%C3%A9%27s+caf%C3%A9
# Debugging encoding problems
def debug_cookie_encoding(cookie)
original_value = cookie.value.first
encoded_header = cookie.to_s
puts "Original value: #{original_value}"
puts "Encoded in header: #{encoded_header}"
puts "URL decode check: #{CGI.unescape(original_value)}"
end
debug_cookie_encoding(encoded_cookie)
Cookie parsing errors occur when browsers send malformed cookie headers or when cookie values become corrupted. The CGI library attempts to parse invalid cookies but may produce unexpected results requiring validation.
# Simulating malformed cookie parsing
def parse_suspicious_cookies(cookie_header)
# Manually parse problematic cookie strings
pairs = cookie_header.split(';').map(&:strip)
pairs.each do |pair|
if pair.include?('=')
name, value = pair.split('=', 2)
begin
# Validate cookie components
raise ArgumentError, "Empty cookie name" if name.empty?
raise ArgumentError, "Cookie name contains spaces" if name.include?(' ')
decoded_value = CGI.unescape(value)
puts "Valid cookie: #{name} = #{decoded_value}"
rescue ArgumentError => e
puts "Invalid cookie ignored: #{pair} (#{e.message})"
end
else
puts "Malformed cookie pair ignored: #{pair}"
end
end
end
# Test with problematic cookie data
malformed_header = "valid=value; =empty_name; spaced name=value; valid2=value2"
parse_suspicious_cookies(malformed_header)
Debugging cookie transmission involves examining both outgoing Set-Cookie headers and incoming Cookie headers. Cookie attributes may be ignored or modified by browsers, requiring server-side validation of cookie behavior.
# Cookie debugging utilities
class CookieDebugger
def self.analyze_cookie(cookie)
analysis = {
name: cookie.name,
values: cookie.value,
expires: cookie.expires,
path: cookie.path,
domain: cookie.domain,
secure: cookie.secure,
httponly: cookie.httponly,
header_length: cookie.to_s.length
}
# Check for common issues
warnings = []
warnings << "Cookie name contains unusual characters" if cookie.name =~ /[^a-zA-Z0-9_-]/
warnings << "Cookie exceeds 4KB limit" if cookie.to_s.length > 4096
warnings << "Secure flag without HTTPS context" if cookie.secure && !https_context?
warnings << "HttpOnly flag may not work in old browsers" if cookie.httponly
analysis[:warnings] = warnings
analysis
end
def self.https_context?
ENV['HTTPS'] == 'on' || ENV['HTTP_X_FORWARDED_PROTO'] == 'https'
end
end
cookie = CGI::Cookie.new('debug_test', 'test_value', {'secure' => true})
debug_info = CookieDebugger.analyze_cookie(cookie)
puts debug_info
Production Patterns
Web application cookie management requires systematic approaches to cookie lifecycle, security configuration, and performance optimization. Production environments demand consistent cookie policies across application instances and proper handling of cookie-related security headers.
# Production cookie management class
class ProductionCookieManager
COOKIE_DEFAULTS = {
secure: true,
httponly: true,
samesite: 'Strict',
path: '/'
}.freeze
def self.create_session_cookie(session_id, expires_in = 3600)
CGI::Cookie.new(
'name' => 'session_id',
'value' => session_id,
'expires' => Time.now + expires_in,
**COOKIE_DEFAULTS
)
end
def self.create_csrf_cookie(token)
CGI::Cookie.new(
'name' => 'csrf_token',
'value' => token,
'expires' => Time.now + 86400, # 24 hours
'secure' => true,
'samesite' => 'Strict',
'path' => '/'
)
end
def self.create_preferences_cookie(preferences_hash)
serialized = preferences_hash.to_json
CGI::Cookie.new(
'name' => 'user_prefs',
'value' => Base64.strict_encode64(serialized),
'expires' => Time.now + (365 * 86400), # 1 year
**COOKIE_DEFAULTS.merge(httponly: false) # Accessible to JavaScript
)
end
end
# Usage in web application
session_cookie = ProductionCookieManager.create_session_cookie('abc123')
csrf_cookie = ProductionCookieManager.create_csrf_cookie('def456')
Cookie security in production requires careful attribute configuration and validation of cookie content. Security headers complement cookie attributes to prevent common attacks like XSS and CSRF while maintaining application functionality.
# Security-focused cookie implementation
class SecureCookieHandler
def self.set_authenticated_user_cookie(cgi, user_id, remember_me = false)
expires_time = remember_me ? Time.now + (30 * 86400) : Time.now + 3600
# Create signed cookie value to prevent tampering
signed_value = sign_cookie_value(user_id.to_s)
cookie = CGI::Cookie.new(
'name' => 'auth_user',
'value' => signed_value,
'expires' => expires_time,
'secure' => true,
'httponly' => true,
'samesite' => 'Strict',
'domain' => ENV['COOKIE_DOMAIN'] || request_domain(cgi),
'path' => '/'
)
cgi.out('cookie' => cookie) { "User authenticated" }
end
def self.verify_cookie_signature(signed_value)
# Simple HMAC verification (use proper crypto library in production)
value, signature = signed_value.split('.')
expected_signature = OpenSSL::HMAC.hexdigest('SHA256', secret_key, value)
if signature == expected_signature
Base64.decode64(value)
else
raise SecurityError, "Cookie signature invalid"
end
end
private
def self.sign_cookie_value(value)
encoded_value = Base64.strict_encode64(value)
signature = OpenSSL::HMAC.hexdigest('SHA256', secret_key, encoded_value)
"#{encoded_value}.#{signature}"
end
def self.secret_key
ENV['COOKIE_SECRET'] || raise("COOKIE_SECRET environment variable required")
end
def self.request_domain(cgi)
cgi.server_name.sub(/^www\./, '') # Remove www prefix for broader domain
end
end
Performance optimization for cookies focuses on minimizing cookie size and reducing redundant cookie transmission. Large cookies increase request overhead, especially for AJAX requests that automatically include all cookies for the domain.
# Cookie performance optimization
class OptimizedCookieStore
MAX_COOKIE_SIZE = 4000 # Conservative browser limit
def self.store_large_data(name, data_hash)
serialized = data_hash.to_json
if serialized.length > MAX_COOKIE_SIZE
# Split large data across multiple cookies
chunks = serialized.scan(/.{1,#{MAX_COOKIE_SIZE - 50}}/) # Leave room for metadata
chunks.each_with_index do |chunk, index|
chunk_cookie = CGI::Cookie.new(
"#{name}_chunk_#{index}",
Base64.strict_encode64(chunk),
{'expires' => Time.now + 86400}
)
yield chunk_cookie # Caller handles cookie output
end
# Create index cookie to track chunks
index_cookie = CGI::Cookie.new(
"#{name}_index",
chunks.length.to_s,
{'expires' => Time.now + 86400}
)
yield index_cookie
else
# Single cookie for smaller data
single_cookie = CGI::Cookie.new(
name,
Base64.strict_encode64(serialized),
{'expires' => Time.now + 86400}
)
yield single_cookie
end
end
def self.retrieve_large_data(cgi, name)
index_cookie = cgi.cookies["#{name}_index"]
return nil unless index_cookie
chunk_count = index_cookie.value.first.to_i
chunks = []
(0...chunk_count).each do |i|
chunk_cookie = cgi.cookies["#{name}_chunk_#{i}"]
return nil unless chunk_cookie # Missing chunk
chunks << Base64.decode64(chunk_cookie.value.first)
end
JSON.parse(chunks.join)
rescue JSON::ParserError
nil # Invalid data
end
end
Common Pitfalls
Cookie name restrictions cause subtle failures when applications use invalid characters or reserved words. Browsers silently ignore malformed cookie names, leading to missing cookies that applications expect to receive.
# Problematic cookie names that fail silently
problematic_names = [
'user name', # Contains space
'user-data;', # Contains semicolon
'user=info', # Contains equals sign
'', # Empty name
'Set-Cookie' # Reserved header name
]
problematic_names.each do |name|
begin
cookie = CGI::Cookie.new(name, 'test_value')
puts "Created cookie: #{name}" # May not actually work in browsers
rescue ArgumentError => e
puts "Failed to create cookie '#{name}': #{e.message}"
end
end
# Safe cookie naming patterns
safe_names = ['user_name', 'userData', 'user123', 'session_id']
safe_names.each do |name|
cookie = CGI::Cookie.new(name, 'test_value')
puts "Safe cookie created: #{name}"
end
Cookie value encoding issues occur when applications assume values remain unchanged during browser transmission. Special characters, Unicode content, and JSON data require proper encoding to survive the cookie round-trip process.
# Encoding pitfalls demonstration
test_values = [
'simple_value', # Works fine
'value with spaces', # Gets URL-encoded
'value;with=special,chars', # Problematic characters
'{"json": "object"}', # JSON needs encoding
'café München', # Unicode characters
'value\nwith\nnewlines' # Control characters
]
test_values.each do |value|
cookie = CGI::Cookie.new('test', value)
header_value = cookie.to_s
# Extract value from Set-Cookie header
extracted = header_value.match(/test=([^;]+)/)[1]
decoded = CGI.unescape(extracted)
puts "Original: #{value.inspect}"
puts "In header: #{extracted}"
puts "Decoded: #{decoded.inspect}"
puts "Match: #{value == decoded}"
puts "---"
end
Domain and path attribute confusion leads to cookies not being sent when expected or being sent to unintended pages. Browser behavior with domain attributes differs from intuitive expectations, especially with subdomain handling.
# Domain attribute pitfalls
def demonstrate_domain_issues
# These domain settings have unexpected behavior
examples = [
{domain: 'example.com', desc: 'Missing dot - only exact domain'},
{domain: '.example.com', desc: 'With dot - includes subdomains'},
{domain: 'www.example.com', desc: 'Subdomain - very restrictive'},
{domain: '.com', desc: 'TLD domain - browsers reject this'},
{domain: '', desc: 'Empty domain - uses current domain'}
]
examples.each do |config|
cookie = CGI::Cookie.new(
'name' => 'domain_test',
'value' => 'test',
'domain' => config[:domain]
)
puts "Domain: '#{config[:domain]}' - #{config[:desc]}"
puts "Set-Cookie: #{cookie.to_s}"
puts "Will be sent to:"
case config[:domain]
when 'example.com'
puts " - example.com only"
when '.example.com'
puts " - example.com and all subdomains"
when 'www.example.com'
puts " - www.example.com only"
when '.com'
puts " - REJECTED by browsers"
when ''
puts " - Current domain only"
end
puts "---"
end
end
demonstrate_domain_issues
Expiration handling creates bugs when applications assume cookie persistence or immediate deletion. Browser behavior with expired cookies varies, and timezone issues affect expiration calculations in server environments.
# Expiration pitfalls and solutions
class CookieExpirationHandler
def self.demonstrate_expiration_issues
# Issue 1: Timezone confusion
utc_time = Time.utc(2025, 12, 31, 23, 59, 59)
local_time = Time.local(2025, 12, 31, 23, 59, 59)
utc_cookie = CGI::Cookie.new('utc_test', 'value', {'expires' => utc_time})
local_cookie = CGI::Cookie.new('local_test', 'value', {'expires' => local_time})
puts "UTC cookie expires: #{utc_cookie.to_s}"
puts "Local cookie expires: #{local_cookie.to_s}"
puts "Time difference may cause unexpected behavior"
# Issue 2: Immediate expiration doesn't guarantee deletion
expired_cookie = CGI::Cookie.new('delete_me', 'value', {'expires' => Time.now - 1})
puts "Expired cookie: #{expired_cookie.to_s}"
puts "Browser may still send this cookie briefly"
end
def self.safe_cookie_deletion(cookie_name)
# Proper cookie deletion technique
delete_cookie = CGI::Cookie.new(
'name' => cookie_name,
'value' => '', # Empty value
'expires' => Time.at(0), # Unix epoch time
'path' => '/', # Match original path
'domain' => '.example.com' # Match original domain
)
delete_cookie
end
def self.create_session_cookie_safely
# Session cookies (no expires) vs short-lived cookies
session_cookie = CGI::Cookie.new('session', 'value') # No expires = session only
# Short-lived alternative with explicit expiration
short_cookie = CGI::Cookie.new(
'name' => 'temporary',
'value' => 'value',
'expires' => Time.now + 300 # 5 minutes from now
)
[session_cookie, short_cookie]
end
end
CookieExpirationHandler.demonstrate_expiration_issues
Cookie size limitations cause silent truncation or complete cookie rejection by browsers. Applications storing complex data in cookies must account for size restrictions and implement fallback storage mechanisms.
# Cookie size management
class CookieSizeManager
BROWSER_LIMITS = {
chrome: 4096,
firefox: 4096,
safari: 4093,
edge: 4096,
conservative: 4000 # Safe limit for all browsers
}
def self.check_cookie_size(cookie)
header_string = cookie.to_s
size = header_string.bytesize
status = {
size: size,
safe: size <= BROWSER_LIMITS[:conservative],
warnings: []
}
if size > BROWSER_LIMITS[:conservative]
status[:warnings] << "Exceeds conservative limit (#{BROWSER_LIMITS[:conservative]} bytes)"
end
if size > BROWSER_LIMITS[:chrome]
status[:warnings] << "May be rejected by most browsers"
end
status
end
def self.compress_cookie_data(data)
# Demonstrate data compression for large cookies
json_data = data.to_json
compressed = Zlib::Deflate.deflate(json_data)
encoded = Base64.strict_encode64(compressed)
puts "Original JSON: #{json_data.length} bytes"
puts "Compressed: #{compressed.length} bytes"
puts "Base64 encoded: #{encoded.length} bytes"
# Check if compression helps with cookie limits
test_cookie = CGI::Cookie.new('compressed_data', encoded)
size_status = check_cookie_size(test_cookie)
puts "Cookie viable: #{size_status[:safe]}"
size_status[:warnings].each { |warning| puts "Warning: #{warning}" }
encoded
end
end
# Test with large data
large_data = {
user_preferences: {
theme: 'dark',
language: 'en-US',
notifications: ['email', 'push', 'sms'],
dashboard_layout: ['weather', 'news', 'calendar', 'stocks'] * 20 # Make it large
}
}
CookieSizeManager.compress_cookie_data(large_data)
Reference
CGI::Cookie Class Methods
Method | Parameters | Returns | Description |
---|---|---|---|
new(name, value, **options) |
name (String), value (String/Array), options (Hash) |
CGI::Cookie |
Creates new cookie instance with specified name and values |
new(**options) |
Hash with 'name', 'value', and attribute keys | CGI::Cookie |
Alternative constructor using hash syntax |
parse(cookie_string) |
cookie_string (String) |
Hash |
Parses HTTP Cookie header into hash of cookie objects |
Instance Methods
Method | Parameters | Returns | Description |
---|---|---|---|
name |
None | String |
Returns cookie name |
name=(string) |
string (String) |
String |
Sets cookie name |
value |
None | Array |
Returns array of cookie values |
value=(array) |
array (Array) |
Array |
Sets cookie values |
domain |
None | String |
Returns domain attribute |
domain=(string) |
string (String) |
String |
Sets domain attribute |
path |
None | String |
Returns path attribute |
path=(string) |
string (String) |
String |
Sets path attribute |
expires |
None | Time |
Returns expiration time |
expires=(time) |
time (Time) |
Time |
Sets expiration time |
secure |
None | Boolean |
Returns secure flag status |
secure=(boolean) |
boolean (Boolean) |
Boolean |
Sets secure flag |
httponly |
None | Boolean |
Returns HttpOnly flag status |
httponly=(boolean) |
boolean (Boolean) |
Boolean |
Sets HttpOnly flag |
to_s |
None | String |
Generates Set-Cookie header string |
Constructor Options
Option | Type | Default | Description |
---|---|---|---|
name |
String | Required | Cookie name identifier |
value |
String/Array | Required | Cookie value(s) |
expires |
Time | nil | Expiration timestamp |
domain |
String | nil | Domain restriction |
path |
String | nil | Path restriction |
secure |
Boolean | false | HTTPS-only transmission |
httponly |
Boolean | false | JavaScript access prevention |
Cookie Attributes Reference
Attribute | Format | Effect | Browser Support |
---|---|---|---|
expires |
RFC 2822 date | Cookie persistence duration | Universal |
max-age |
Seconds | Alternative to expires | Modern browsers |
domain |
Domain string | Domain scope control | Universal |
path |
Path string | URL path restriction | Universal |
secure |
Flag only | HTTPS transmission only | Universal |
httponly |
Flag only | Prevents JavaScript access | IE6+ |
samesite |
Strict/Lax/None | CSRF protection | Modern browsers |
Common Cookie Patterns
# Session cookie (expires when browser closes)
session = CGI::Cookie.new('session_id', 'abc123')
# Persistent cookie with expiration
persistent = CGI::Cookie.new('user_pref', 'value', {
'expires' => Time.now + 86400
})
# Secure authentication cookie
auth = CGI::Cookie.new('auth_token', 'token', {
'expires' => Time.now + 3600,
'secure' => true,
'httponly' => true,
'path' => '/admin'
})
# Multi-value cookie
multi = CGI::Cookie.new('selections', ['item1', 'item2', 'item3'])
# Domain-wide cookie
domain_cookie = CGI::Cookie.new('global_pref', 'value', {
'domain' => '.example.com',
'path' => '/'
})
Error Types and Handling
Error | Cause | Solution |
---|---|---|
ArgumentError |
Invalid cookie name | Use alphanumeric names with underscores/hyphens |
ArgumentError |
Nil cookie name | Provide non-empty string for name parameter |
Size limit exceeded | Cookie header > 4KB | Compress data or use server-side storage |
Encoding issues | Non-ASCII characters | Allow automatic URL encoding or pre-encode |
Parse failures | Malformed cookie headers | Validate input before parsing |
Browser Compatibility Matrix
Feature | Chrome | Firefox | Safari | Edge | IE11 |
---|---|---|---|---|---|
Basic cookies | ✓ | ✓ | ✓ | ✓ | ✓ |
HttpOnly | ✓ | ✓ | ✓ | ✓ | ✓ |
Secure | ✓ | ✓ | ✓ | ✓ | ✓ |
SameSite | ✓ | ✓ | ✓ | ✓ | ✗ |
Max cookie size | 4096 | 4096 | 4093 | 4096 | 4096 |
Max cookies per domain | 180 | 150 | ~600 | 180 | 50 |