CrackedRuby CrackedRuby

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
pdf application/pdf .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