Overview
Content negotiation describes the process where HTTP clients and servers exchange information to determine the most appropriate representation of a resource. Rather than serving a single fixed format, servers can provide multiple representations of the same resource—JSON, XML, HTML, plain text, or various other formats—and select the one that best matches the client's capabilities and preferences.
The HTTP protocol defines content negotiation through a set of request and response headers. Clients express their preferences using Accept headers, while servers indicate their capabilities through Content-Type and other response headers. This mechanism operates transparently to end users while providing flexibility for API consumers, browser clients, and automated systems.
Content negotiation occurs in two primary forms: server-driven and agent-driven. Server-driven negotiation examines request headers to select an appropriate representation before generating the response. Agent-driven negotiation returns multiple choices to the client, allowing the client to select the preferred option. Modern web applications predominantly use server-driven negotiation due to its simplicity and efficiency.
The mechanism addresses a fundamental challenge in distributed systems: different clients have different requirements. A web browser rendering a user interface requires HTML, a mobile application needs JSON, a reporting system might request CSV data, and a legacy system could require XML. Content negotiation allows a single endpoint to serve all these clients without maintaining separate URLs for each format.
# Basic content negotiation in a Ruby web application
def show
user = User.find(params[:id])
respond_to do |format|
format.html { render :show }
format.json { render json: user }
format.xml { render xml: user }
end
end
Key Principles
Content negotiation operates on the principle of proactive selection, where the server analyzes client preferences before generating a response. This differs from reactive selection, where the server generates multiple variants and lets the client choose. The HTTP specification defines several dimensions along which negotiation occurs: media type, character encoding, content language, and transfer encoding.
Quality factors form the mathematical foundation of content negotiation. Quality values range from 0.0 to 1.0, indicating relative preference levels. A client can specify multiple acceptable formats with different quality values, creating a preference hierarchy. The server selects the representation with the highest combined quality score that it can produce.
# Client sends: Accept: application/json;q=0.9, text/html;q=1.0, application/xml;q=0.5
# Server interprets: HTML preferred (1.0), JSON acceptable (0.9), XML least preferred (0.5)
The media type dimension defines the format and structure of the representation. Media types follow the pattern type/subtype, such as application/json or text/html. Wildcard patterns enable broader matching: text/* accepts any text format, while */* accepts any format. Servers should interpret wildcards as lower priority than specific type declarations.
Transparent content negotiation requires that the server's selection algorithm be deterministic and consistent. Given identical request headers, the server must return the same representation format. This consistency enables caching proxies to store and serve negotiated responses correctly. The Vary response header indicates which request headers influenced the selection, allowing caches to key responses appropriately.
Language negotiation occurs independently from format negotiation. A resource might exist in multiple languages, and clients specify language preferences through the Accept-Language header. Language codes follow ISO 639 standards, with optional regional qualifiers. The negotiation process matches client preferences against available translations, considering both exact matches and language-family fallbacks.
The principle of graceful degradation dictates server behavior when no exact match exists. Rather than failing with an error, servers should return the closest available representation or a default format. The server communicates this partial match through status codes: 200 for exact matches, 300 for multiple choices, or 406 when no acceptable representation exists.
Encoding negotiation handles character encoding and content compression separately. The Accept-Charset header specifies character encoding preferences, while Accept-Encoding handles compression algorithms like gzip or deflate. These operate independently from media type negotiation, allowing combinations like gzip-compressed UTF-8 JSON.
Content negotiation distinguishes between representation metadata and resource identity. Different representations of a resource share the same URI, maintaining a single logical identity across formats. This contrasts with approaches that use different URIs for different formats (like /users/1.json vs /users/1.xml), which fragment resource identity and complicate caching.
Ruby Implementation
Ruby web frameworks provide multiple approaches to content negotiation, ranging from manual header parsing to declarative format handling. The Rack library forms the foundation, providing request and response abstractions that higher-level frameworks build upon.
Rails implements content negotiation through the respond_to method and the ActionController::MimeResponds module. This approach declares supported formats explicitly and maps each format to a rendering action:
class ArticlesController < ApplicationController
def show
@article = Article.find(params[:id])
respond_to do |format|
format.html
format.json { render json: @article }
format.xml { render xml: @article }
format.pdf { render pdf: generate_pdf(@article) }
format.csv { render csv: export_csv(@article) }
end
end
end
The framework parses the Accept header automatically and selects the first matching format from the respond_to block. When no format matches, Rails returns a 406 Not Acceptable response. Format precedence follows declaration order in the absence of quality factors.
Sinatra adopts a more lightweight approach, requiring manual content negotiation or helper methods:
require 'sinatra'
require 'json'
get '/users/:id' do
user = User.find(params[:id])
case request.accept.first.to_s
when /json/
content_type :json
user.to_json
when /xml/
content_type :xml
user.to_xml
else
content_type :html
erb :user, locals: { user: user }
end
end
The rack-accept gem provides sophisticated content negotiation for any Rack-based application. It implements complete RFC 7231 semantics, including quality factor parsing and wildcard matching:
require 'rack/accept'
class API
def call(env)
request = Rack::Request.new(env)
accept = Rack::Accept::MediaType.new(env['HTTP_ACCEPT'])
if accept.best_of(['application/json', 'application/xml']) == 'application/json'
[200, { 'Content-Type' => 'application/json' }, [generate_json]]
else
[200, { 'Content-Type' => 'application/xml' }, [generate_xml]]
end
end
end
Custom MIME types require registration with Rails before content negotiation can recognize them:
# config/initializers/mime_types.rb
Mime::Type.register "application/vnd.myapp+json", :myapp_json
Mime::Type.register "application/vnd.myapp.v2+json", :myapp_json_v2
# Controller can now negotiate these types
respond_to do |format|
format.myapp_json { render json: v1_format(@resource) }
format.myapp_json_v2 { render json: v2_format(@resource) }
end
Quality factor parsing requires manual implementation in Sinatra or custom Rack applications:
def parse_accept_header(accept_header)
return [['*/*', 1.0]] if accept_header.nil? || accept_header.empty?
accept_header.split(',').map do |part|
media_type, *params = part.split(';').map(&:strip)
quality = params.find { |p| p.start_with?('q=') }
q_value = quality ? quality.split('=').last.to_f : 1.0
[media_type, q_value]
end.sort_by { |_, q| -q }
end
def best_match(accept_header, available_types)
preferences = parse_accept_header(accept_header)
preferences.each do |media_type, _|
return media_type if available_types.include?(media_type)
# Handle wildcards
if media_type.end_with?('/*')
base_type = media_type.split('/').first
match = available_types.find { |t| t.start_with?("#{base_type}/") }
return match if match
end
return available_types.first if media_type == '*/*'
end
nil
end
Rails provides format detection through file extensions as an alternative to header-based negotiation. Appending .json or .xml to a URL explicitly requests that format:
# Both requests work identically:
# GET /articles/1.json
# GET /articles/1 with Accept: application/json
respond_to do |format|
format.json { render json: @article }
end
The Vary header instructs caching layers about negotiation dependencies:
def show
@article = Article.find(params[:id])
response.headers['Vary'] = 'Accept, Accept-Language'
respond_to do |format|
format.json { render json: @article }
format.html { render :show }
end
end
Practical Examples
API Versioning Through Content Types: Modern APIs use vendor-specific MIME types to negotiate API versions without polluting URLs:
# config/initializers/mime_types.rb
Mime::Type.register "application/vnd.bookstore.v1+json", :v1_json
Mime::Type.register "application/vnd.bookstore.v2+json", :v2_json
Mime::Type.register "application/vnd.bookstore.v3+json", :v3_json
class BooksController < ApplicationController
def show
@book = Book.find(params[:id])
respond_to do |format|
format.v1_json { render json: V1::BookSerializer.new(@book) }
format.v2_json { render json: V2::BookSerializer.new(@book) }
format.v3_json { render json: V3::BookSerializer.new(@book) }
format.json { render json: latest_serializer(@book) }
end
end
private
def latest_serializer(book)
V3::BookSerializer.new(book)
end
end
# Client requests:
# Accept: application/vnd.bookstore.v1+json
# Accept: application/vnd.bookstore.v2+json
# Accept: application/json (gets v3, the latest)
Multi-Language Documentation System: Content negotiation selects documentation language based on client preferences:
class DocumentationController < ApplicationController
def show
@doc = Documentation.find(params[:id])
# Negotiate language
preferred_locale = http_accept_language.compatible_language_from(I18n.available_locales)
I18n.locale = preferred_locale || I18n.default_locale
# Set Vary header for proper caching
response.headers['Vary'] = 'Accept-Language'
respond_to do |format|
format.html { render :show }
format.json do
render json: {
title: @doc.title,
content: @doc.content,
language: I18n.locale
}
end
end
end
end
# Client sends: Accept-Language: fr-FR,fr;q=0.9,en;q=0.8
# Server responds with French content if available, otherwise English
Data Export with Format Negotiation: A reporting system exports data in multiple formats based on client requirements:
class ReportsController < ApplicationController
def export
@report = Report.find(params[:id])
@data = @report.generate_data
respond_to do |format|
format.json do
render json: {
report: @report.title,
data: @data,
generated_at: Time.current
}
end
format.csv do
send_data generate_csv(@data),
filename: "#{@report.title}-#{Date.current}.csv",
type: 'text/csv',
disposition: 'attachment'
end
format.xlsx do
send_data generate_xlsx(@data),
filename: "#{@report.title}-#{Date.current}.xlsx",
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
disposition: 'attachment'
end
format.pdf do
render pdf: @report.title,
template: 'reports/export.pdf.erb',
layout: 'pdf'
end
end
end
private
def generate_csv(data)
CSV.generate do |csv|
csv << data.first.keys
data.each { |row| csv << row.values }
end
end
def generate_xlsx(data)
package = Axlsx::Package.new
workbook = package.workbook
workbook.add_worksheet(name: "Report") do |sheet|
sheet.add_row data.first.keys
data.each { |row| sheet.add_row row.values }
end
package.to_stream.read
end
end
Webhook Payload Format Negotiation: A webhook system delivers notifications in formats specified by subscribers:
class WebhookDelivery
def deliver(webhook, event)
uri = URI(webhook.url)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = uri.scheme == 'https'
request = Net::HTTP::Post.new(uri.path)
request['User-Agent'] = 'WebhookSystem/1.0'
# Send in format specified by webhook configuration
case webhook.preferred_format
when 'json'
request['Content-Type'] = 'application/json'
request.body = event.to_json
when 'xml'
request['Content-Type'] = 'application/xml'
request.body = event.to_xml
when 'form'
request['Content-Type'] = 'application/x-www-form-urlencoded'
request.body = URI.encode_www_form(event.to_h)
end
response = http.request(request)
log_delivery(webhook, response)
end
def log_delivery(webhook, response)
WebhookLog.create(
webhook: webhook,
status_code: response.code,
content_type: response['Content-Type'],
response_body: response.body
)
end
end
Common Patterns
Default Format Strategy: APIs establish a default format for clients that don't specify preferences:
class ApiController < ApplicationController
before_action :set_default_format
private
def set_default_format
request.format = :json unless params[:format] || request.headers['Accept']
end
end
# Alternatively, set default in routes
namespace :api do
defaults format: :json do
resources :users
resources :posts
end
end
Quality Factor Preference Pattern: Sophisticated clients express nuanced preferences through quality factors:
# Client sends:
# Accept: application/json;q=1.0, application/xml;q=0.8, text/html;q=0.5, */*;q=0.1
def select_format(accept_header, available)
preferences = parse_quality_factors(accept_header)
best_match = available.map do |format|
quality = preferences[format] ||
preferences[format.split('/').first + '/*'] ||
preferences['*/*'] ||
0
[format, quality]
end.max_by { |_, q| q }
best_match&.first
end
def parse_quality_factors(header)
header.split(',').each_with_object({}) do |part, hash|
media_type, *params = part.split(';').map(&:strip)
quality = params.find { |p| p.start_with?('q=') }
q_value = quality ? quality.split('=').last.to_f : 1.0
hash[media_type] = q_value
end
end
Conditional Format Availability Pattern: Certain formats only generate for specific conditions:
class ArticlesController < ApplicationController
def show
@article = Article.find(params[:id])
respond_to do |format|
format.html
format.json { render json: @article }
# PDF only for published articles
format.pdf do
if @article.published?
render pdf: generate_pdf(@article)
else
head :not_acceptable
end
end
# Admin-only formats
format.xml do
if current_user&.admin?
render xml: @article.to_xml(include_metadata: true)
else
head :forbidden
end
end
end
end
end
Fallback Chain Pattern: Applications specify a preference order when the requested format is unavailable:
class ResourceController < ApplicationController
def show
@resource = Resource.find(params[:id])
respond_to do |format|
format.json { render json: @resource }
format.xml { render xml: @resource }
format.html { render :show }
# Fallback: if client accepts anything, return JSON
format.any do
if request.accepts.include?('*/*')
render json: @resource, status: :ok
else
head :not_acceptable
end
end
end
end
end
Content Type Header Override Pattern: Allow clients to override automatic negotiation for testing or debugging:
class ApplicationController < ActionController::Base
before_action :check_format_override
private
def check_format_override
if params[:_format] && %w[json xml html].include?(params[:_format])
request.format = params[:_format]
end
end
end
# Enables requests like:
# GET /articles/1?_format=json
# Even if Accept header specifies something different
Symmetric Negotiation Pattern: APIs accept the same formats for input and output:
class PostsController < ApplicationController
def create
content_type = request.content_type
@post = case content_type
when /json/
Post.new(JSON.parse(request.body.read))
when /xml/
Post.from_xml(request.body.read)
when /x-www-form-urlencoded/
Post.new(params[:post])
else
return head :unsupported_media_type
end
if @post.save
respond_to do |format|
format.json { render json: @post, status: :created }
format.xml { render xml: @post, status: :created }
end
else
respond_to do |format|
format.json { render json: @post.errors, status: :unprocessable_entity }
format.xml { render xml: @post.errors, status: :unprocessable_entity }
end
end
end
end
Security Implications
Content negotiation introduces security considerations that require careful handling. Media type injection attacks occur when applications generate dynamic content based on negotiated types without proper validation. An attacker might request a format that bypasses security filters or triggers unintended code paths.
# VULNERABLE: Directly using user-influenced format selection
def show
@data = sensitive_data
format_type = params[:format] || request.format
# Attacker could inject unexpected formats
send_data @data.send("to_#{format_type}"), type: format_type
end
# SECURE: Whitelist allowed formats
ALLOWED_FORMATS = %w[json xml csv].freeze
def show
@data = sensitive_data
format_type = params[:format] || request.format.symbol.to_s
unless ALLOWED_FORMATS.include?(format_type)
return head :not_acceptable
end
respond_to do |format|
format.json { render json: @data }
format.xml { render xml: @data }
format.csv { render csv: @data }
end
end
Information disclosure through error messages becomes a risk when different formats expose different levels of detail. XML serialization might include more object attributes than JSON, inadvertently exposing sensitive data:
# VULNERABLE: Different formats expose different data
respond_to do |format|
format.json { render json: @user }
format.xml { render xml: @user.to_xml } # Might include hidden attributes
end
# SECURE: Consistent serialization across formats
respond_to do |format|
format.json { render json: UserSerializer.new(@user).to_json }
format.xml { render xml: UserSerializer.new(@user).to_xml }
end
class UserSerializer
def initialize(user)
@user = user
end
def serializable_hash
{
id: @user.id,
name: @user.name,
email: @user.email
# Explicitly excludes sensitive fields like password_digest
}
end
def to_json(*args)
serializable_hash.to_json(*args)
end
def to_xml(*args)
serializable_hash.to_xml(*args)
end
end
Content type confusion attacks exploit inconsistencies between declared content types and actual content. Browsers and clients make security decisions based on content types, so mismatches create vulnerabilities:
# VULNERABLE: Content type doesn't match actual content
def download
file_data = params[:data]
send_data file_data, type: 'text/plain' # But file_data might contain HTML
end
# SECURE: Validate content matches declared type
def download
file_data = params[:data]
content_type = detect_content_type(file_data)
unless safe_content_type?(content_type)
return head :unprocessable_entity
end
send_data file_data,
type: content_type,
disposition: 'attachment' # Force download instead of rendering
end
def safe_content_type?(type)
%w[text/plain application/json application/pdf].include?(type)
end
XML External Entity (XXE) attacks target applications that parse XML without disabling external entity processing. Content negotiation that accepts XML input must configure parsers defensively:
# VULNERABLE: Default XML parsing allows XXE
def create
respond_to do |format|
format.xml do
data = Hash.from_xml(request.body.read)
@resource = Resource.new(data['resource'])
@resource.save
end
end
end
# SECURE: Disable external entities in XML parser
require 'nokogiri'
def create
respond_to do |format|
format.xml do
xml_doc = Nokogiri::XML(request.body.read) do |config|
config.nonet # Disable network access
config.noent # Disable entity expansion
end
data = xml_to_hash(xml_doc)
@resource = Resource.new(data)
@resource.save
end
end
end
Cross-Site Scripting (XSS) through format selection occurs when user-supplied content renders differently across formats. HTML format might execute scripts, while JSON format safely encodes them:
# VULNERABLE: User content renders without escaping
def show
@comment = Comment.find(params[:id])
respond_to do |format|
format.html { render inline: @comment.body } # XSS if body contains scripts
format.json { render json: @comment }
end
end
# SECURE: Proper escaping for each format
def show
@comment = Comment.find(params[:id])
respond_to do |format|
format.html { render :show } # Uses ERB escaping
format.json { render json: @comment } # JSON encoding prevents XSS
format.text { render plain: @comment.body } # Plain text, no interpretation
end
end
Error Handling & Edge Cases
Content negotiation failures require explicit handling to provide meaningful feedback. The 406 Not Acceptable status code indicates that no available representation matches client requirements:
class ApplicationController < ActionController::Base
rescue_from ActionController::UnknownFormat do |exception|
respond_to do |format|
format.json do
render json: {
error: 'Not Acceptable',
message: 'The requested format is not available',
available_formats: available_formats_for_action
}, status: :not_acceptable
end
format.any do
render plain: 'Not Acceptable', status: :not_acceptable
end
end
end
private
def available_formats_for_action
action_name = params[:action]
controller_name = params[:controller]
# Return list of formats this action supports
['json', 'xml', 'html']
end
end
Empty or malformed Accept headers require defensive handling. Some clients send invalid header values or omit headers entirely:
def handle_negotiation
accept_header = request.headers['Accept']
# Handle missing header
if accept_header.blank?
request.format = :json
return
end
# Handle malformed header
begin
parsed = parse_accept_header(accept_header)
rescue StandardError => e
Rails.logger.warn "Invalid Accept header: #{accept_header} - #{e.message}"
request.format = :json
return
end
# Continue with normal negotiation
negotiate_format(parsed)
end
def parse_accept_header(header)
header.split(',').map do |part|
media_type, *params = part.split(';').map(&:strip)
raise ArgumentError, "Invalid media type" if media_type.blank?
quality = params.find { |p| p.start_with?('q=') }
q_value = quality ? Float(quality.split('=').last) : 1.0
raise ArgumentError, "Invalid quality value" unless (0.0..1.0).cover?(q_value)
[media_type, q_value]
end
end
Wildcard negotiation ambiguity arises when clients specify */* without quality factors. The server must decide which format to return:
def resolve_wildcard_request
accept = request.headers['Accept']
# Client accepts anything
if accept == '*/*' || accept.blank?
# Use explicit ordering
respond_to do |format|
format.json { render json: @resource } # First choice
format.xml { render xml: @resource } # Second choice
format.html { render :show } # Last choice
end
return
end
# Normal negotiation
respond_to do |format|
format.json { render json: @resource }
format.xml { render xml: @resource }
format.html { render :show }
end
end
Circular content type dependencies occur when format generation depends on other negotiated formats:
# PROBLEMATIC: PDF generation requires HTML rendering
def show
@report = Report.find(params[:id])
respond_to do |format|
format.html { render :show }
format.pdf do
# This internally renders HTML first, then converts to PDF
# Creates implicit dependency that might fail
render pdf: "report", template: "reports/show"
end
end
end
# BETTER: Make dependencies explicit and handle failures
def show
@report = Report.find(params[:id])
respond_to do |format|
format.html { render :show }
format.pdf do
begin
html_content = render_to_string(:show, layout: 'pdf')
pdf_data = generate_pdf_from_html(html_content)
send_data pdf_data, type: 'application/pdf'
rescue StandardError => e
Rails.logger.error "PDF generation failed: #{e.message}"
render json: { error: 'PDF generation unavailable' }, status: :internal_server_error
end
end
end
end
Character encoding mismatches between declared and actual encodings cause parsing errors:
def create
respond_to do |format|
format.json do
begin
# Explicitly specify encoding
body = request.body.read.force_encoding('UTF-8')
unless body.valid_encoding?
render json: { error: 'Invalid character encoding' },
status: :bad_request
return
end
data = JSON.parse(body)
@resource = Resource.create(data)
render json: @resource, status: :created
rescue JSON::ParserError => e
render json: { error: 'Invalid JSON', details: e.message },
status: :bad_request
rescue Encoding::InvalidByteSequenceError => e
render json: { error: 'Character encoding error', details: e.message },
status: :bad_request
end
end
end
end
Partial content negotiation failures happen when some requested characteristics can be satisfied but others cannot:
def show
@document = Document.find(params[:id])
# Client wants: Accept: application/json, Accept-Language: fr-FR
# Document exists in JSON but only in English
requested_locale = http_accept_language.compatible_language_from([:en, :es, :de])
available_in_french = @document.translations.exists?(locale: 'fr')
respond_to do |format|
format.json do
response.headers['Content-Language'] = requested_locale || 'en'
if requested_locale
render json: @document.as_json(locale: requested_locale)
else
# Return content in default language with warning
render json: {
data: @document.as_json(locale: 'en'),
warning: 'Requested language unavailable, returned default'
}, status: :ok
end
end
end
end
Reference
Content Negotiation Headers
| Header | Direction | Purpose | Example |
|---|---|---|---|
| Accept | Request | Specifies acceptable media types | application/json, text/html;q=0.9 |
| Accept-Charset | Request | Specifies acceptable character encodings | utf-8, iso-8859-1;q=0.5 |
| Accept-Encoding | Request | Specifies acceptable content encodings | gzip, deflate, br |
| Accept-Language | Request | Specifies acceptable languages | en-US, en;q=0.9, fr;q=0.8 |
| Content-Type | Response | Indicates media type of response | application/json; charset=utf-8 |
| Content-Language | Response | Indicates language of response | en-US |
| Content-Encoding | Response | Indicates encoding applied to response | gzip |
| Vary | Response | Lists headers that affected response selection | Accept, Accept-Language |
HTTP Status Codes
| Code | Name | Usage in Content Negotiation |
|---|---|---|
| 200 | OK | Successful negotiation, returning requested format |
| 300 | Multiple Choices | Multiple representations available, agent-driven negotiation |
| 406 | Not Acceptable | No representation matches Accept headers |
| 415 | Unsupported Media Type | Request Content-Type not supported |
| 422 | Unprocessable Entity | Valid format but invalid content |
Quality Factor Values
| Value | Meaning | Usage |
|---|---|---|
| 1.0 | Highest preference | Default when q not specified |
| 0.9 | High preference | Acceptable but not preferred |
| 0.5 | Medium preference | Usable but less desirable |
| 0.1 | Low preference | Only if nothing else available |
| 0.0 | Not acceptable | Explicitly rejected |
Rails Format Symbols
| Symbol | MIME Type | File Extension |
|---|---|---|
| html | text/html | .html |
| json | application/json | .json |
| xml | application/xml | .xml |
| js | text/javascript | .js |
| css | text/css | .css |
| rss | application/rss+xml | .rss |
| atom | application/atom+xml | .atom |
| csv | text/csv | .csv |
| application/pdf | ||
| zip | application/zip | .zip |
Common Media Type Patterns
| Pattern | Matches | Example |
|---|---|---|
| type/subtype | Exact media type | application/json |
| type/* | Any subtype of type | text/* matches text/html, text/plain |
| / | Any media type | Universal wildcard |
| type/subtype+suffix | Structured type with suffix | application/vnd.api+json |
Negotiation Algorithm Steps
| Step | Action | Result |
|---|---|---|
| 1 | Parse Accept header into preferences list | Ordered list with quality factors |
| 2 | Parse available server formats | List of formats server can generate |
| 3 | Match client preferences against available formats | Find overlapping formats |
| 4 | Sort matches by quality factor descending | Prioritize higher quality matches |
| 5 | Select highest quality match | Return format or 406 if no match |
| 6 | Generate response in selected format | Serialize resource appropriately |
| 7 | Set Content-Type and Vary headers | Enable proper caching |
Rails respond_to Method Syntax
| Pattern | Description | Example |
|---|---|---|
| format.symbol | Handle specific format | format.json { render json: @resource } |
| format.symbol condition | Conditional format handling | format.pdf if can_export_pdf? |
| format.any | Fallback for any unmatched format | format.any { head :not_acceptable } |
| Multiple blocks | Handle multiple formats | format.json {...}; format.xml {...} |
Security Checklist
| Check | Purpose | Implementation |
|---|---|---|
| Whitelist allowed formats | Prevent format injection | Use respond_to with explicit formats |
| Validate content type matches content | Prevent confusion attacks | Verify content before setting type |
| Disable XML external entities | Prevent XXE attacks | Configure parser with noent, nonet |
| Escape output for each format | Prevent XSS | Use format-specific escaping |
| Consistent serialization across formats | Prevent information disclosure | Use serializers for all formats |
| Set appropriate Content-Disposition | Control browser behavior | Use attachment for downloads |
| Validate character encoding | Prevent encoding attacks | Check valid_encoding? on input |