...

HTTP Content Negotiation in Hosting: Optimal Server Response Format

HTTP Content Negotiation fits the server response format in the hosting automatically adapts to the client's requirements and evaluates headers such as Accept, Accept-Language and Accept-Encoding. Depending on the header, I deliver the best variant - such as JSON instead of XML, Gzip or Brotli and the correct language - and thus strengthen the web optimization noticeable.

Key points

The following key points provide a quick overview before I explain the implementation step by step.

  • Header control format, language, character set and compression.
  • Server-driven Negotiation shortens round trips and speeds up delivery.
  • Vary header prevents cache confusion and keeps variants cleanly separated.
  • Fallbacks with JSON/HTML and status 406 ensure predictable behavior.
  • q-values control priorities if several variants are possible.

What is HTTP content negotiation in hosting?

I use Content Negotiation, to deliver a resource in the best possible variant without building multiple endpoints. The client sends preferences in Accept, Accept-Language, Accept-Charset and Accept-Encoding headers, and I respond with the appropriate server response format. For example, a browser receives HTML, a bot JSON and an image client WebP or AVIF. In hosting setups, server-driven negotiation dominates because it does not trigger any additional round trips and responds directly to the headers. If no suitable variant remains, I respond consistently with 406 Not Acceptable so that clients receive a clear signal.

Request and response headers at a glance

For reliable negotiation, I always pay attention to two sides: The incoming Request header with preferences and the outgoing response headers with unique identification. Accept shows permitted media types, Accept-Language preferred languages, Accept-Charset the character set and Accept-Encoding possible compressions. I set up the response with Content-Type, Content-Language, Content-Encoding and the correct Vary header so that caches do not serve incorrect variants. The Vary header tells caches which characteristics they need to use to distinguish between variants, such as Vary: Accept, Accept-Language. If you use content negotiation http, you should maintain this header combination consistently, otherwise errors will occur in the cache.

Header Purpose Example Important answer Cache hint
Accept Permitted media types application/json; q=0.9, text/html; q=0.8 Content-Type: application/json Vary: Accept
Accept-Language Preferred languages de-DE, en-US; q=0.7 Content-Language: de-DE Vary: Accept-Language
Accept charset Character set utf-8 Content-Type: text/html; charset=utf-8 Vary: Accept charset
Accept-Encoding Compression br, gzip; q=0.8 Content-Encoding: br Vary: Accept-Encoding

Server-driven, client-driven and request-driven

I differentiate between three approaches and, depending on the project, choose the suitable Model. Server-driven (proactive) is my standard, as the server decides directly based on the headers and immediately returns a variant. Client-driven (reactive) lets the client choose from a list, but generates additional work due to additional requests. Request-driven mixes both, for example by counting parameters in the URL together with Accept headers. For hosting environments with a high load, server-driven behavior is convincing because it saves round trips, relieves caches and allows clear rules.

Apache: .htaccess, MultiViews and type maps

On Apache I activate MultiViews or use type maps to automatically serve language and format variants. MultiViews allows file pairs such as index.html.de and index.html.en, which Apache selects based on Accept-Language. I set q values for media types so that modern formats are given priority, such as image/webp over image/jpeg. I always make sure to set Vary correctly and deliver 406 if clients request an unsupported format. This keeps the behavior predictable and prevents caches from storing conflicting responses.

# .htaccess
Options +MultiViews

# Example for Type-Map (file.var)
URI: image
Content-type: image/webp; qs=0.9
Content-type: image/jpeg; qs=0.8
Content-language: de

# Automatically operate language variant
# files: index.html.de, index.html.en

Nginx: map, Lua and edge logic

Under Nginx I often set map-directives to evaluate Accept headers and assign suitable endpoints. For APIs, I redirect between HTML and JSON depending on Accept, optionally supplemented by Lua for finer rules. I keep an eye on the Vary header, because caches have to link decisions to Accept and Accept-Language. In distributed setups, I move parts of the negotiation to edge nodes to keep latencies low. A whitelist remains important so that I only offer verified media types and don't fall for exotic formats.

# nginx.conf (excerpt)
map $http_accept $fmt {
  default "html";
  "~*application/json" "json";
  "~*\\*/\\*" "json";
}

server {
  add_header Vary "Accept, Accept-Language";
  location /api {
    try_files $uri $uri/ /api.$fmt;
  }
}

Caching, Vary and SEO signals

Without correct Vary-headers, caches behave unpredictably and deliver incorrect variants to other users. I set Vary exactly to the headers that I use to differentiate, i.e. typically Accept, Accept-Language and Accept-Encoding. This not only strengthens consistency, but also sends clear signals for performance, which indirectly brings SEO benefits. Those who want to delve deeper into header strategies will benefit from this guide to HTTP headers for performance and SEO. I also check whether the CDN cache key maps these dimensions so that edge nodes hold the correct objects.

APIs: Whitelisting formats and clean fallback

With APIs, I keep the supported media types in a Whitelist for example, application/json and application/xml. If the Accept header is missing or nothing fits, I supply JSON as the default, as it is the most widely supported. If a client explicitly requests an unknown format, I respond with 406 Not Acceptable instead of guessing silently. A user's profile settings take precedence over Accept if the application specifies this. I ensure that these rules are centralized, reproducible and validated by means of tests so that integrations remain stable.

Languages, fonts and accessibility

For Multilingualism I use Accept-Language to automatically select language variants and mark Content-Language in the response. I formulate fallbacks clearly: if the desired language does not exist, I use a defined standard language. I use Accept-Charset to ensure that UTF-8 applies everywhere so that special characters appear consistently. Screen readers also benefit from correct language names in the content language and lang attributes in the markup. This keeps the delivery inclusive, transparent and technically clean.

Images, compression and media types

When it comes to pictures, I give modern formats a Head start and pay attention to the Accept headers of browsers. If the client supports AVIF or WebP, I prefer to deliver these versions, otherwise I choose JPEG or PNG. This practical guide helps me to decide between WebP and AVIF WebP vs AVIF comparison. In addition, I reduce the amount of data significantly via accept encoding with Brotli or Gzip, in practice often up to 50 %. This saves bandwidth, shortens time-to-first-byte and stabilizes the perceived speed.

Measure, test, roll out

I measure the effect of negotiation on an ongoing basis, otherwise potential remains unused. I use curl to check variants, such as curl -H „Accept: application/json“ or curl -H „Accept-Language: de“. I check hit rates per variant in logs and compare them with CDN statistics. For encoding strategies and Brotli grades, I compare result curves before setting global defaults. This guide to setup and tuning provides me with a compact introduction to the Configure HTTP compression, which I coordinate in parallel with the negotiation.

Error codes and edge cases in practice

I make a clear distinction between 406 Not Acceptable and 415 Unsupported Media Type: I set 406 if the Answer is not available in an accepted variant (Accept denied); I use 415 if the Request sends an unsupported media type (content type of the request payload). In rare cases, 300 Multiple Choices makes sense if I want to offer the client several exactly matching variants - in practice, however, I use clear defaults instead of interactive selection in high-load environments. With caching, I continue to respond with 304 Not Modified per variant; ETag and Last-Modified always apply variant-specifically. If Accept is missing completely, I interpret this as „everything is allowed“ and use the defined default (usually JSON for APIs, HTML for websites). If a client sets q=0 for a type, I explicitly exclude this variant.

Security: sniffing, whitelists and input hygiene

I don't let the browser „guess“ the content type, but lie with consistent content type and X-Content-Type-Options: nosniff fixed. In the negotiate logic, I only accept whitelisted types/languages and limit header lengths so that unusually long accept-language lists do not tie up resources. For logs and metrics, I clean up header values to avoid injection risks. I also pay attention to data protection: Accept-Language can allow conclusions to be drawn about users; I only save as much as necessary and aggregate for statistics. With CORS, I let negotiation decide independently - I bind cross-origin rules separately to Origin/Methods/Headers, not to Accept variants, so that I don't generate any unintentional releases.

CDN, cache keys and ETags per variant

With CDNs, I deliberately define the cache key to be variable. In addition to URL, this includes Accept, Accept-Language and Accept-Encoding, exactly as I signal in the Vary header. I set my own ETags for each variant (e.g. hash with suffix „.json.de.br“) and ensure that conditional requests work correctly. For static assets, I work with pre-generated, compressed files (br/gz) that the CDN serves 1:1. To reduce origin load, I use „collapsed forwarding“ or „stale-while-revalidate“: The first miss is updated, all others receive a fresh or „stale acceptable“ variant. I only combine range requests with compression if the server and CDN handle the feature consistently; otherwise I deactivate range for dynamically compressed responses to avoid fragmentation of the variants.

q-values, wildcards and matching algorithm

If several variants apply, I sort by q values and precision: exact type/subtype beats type/*, both beat */*. If q is the same, the more specific variant wins. If the client does not set a q value, I interpret it as 1.0. With q=0, the client explicitly excludes a type. For images and documents, I give preference to modern formats with a slightly higher q, but offer fallbacks if the client does not recognize AVIF, for example.

# Pseudocode for accept matching
parse acceptHeader into candidates (type, subtype, q)
for variant in serverVariants:
  score = 0
  for cand in candidates:
    if cand.type == variant.type and cand.subtype == variant.subtype:
      score = max(score, 1000 * cand.q + 2) # exactly
    elif cand.type == variant.type and cand.subtype == "*":
      score = max(score, 1000 * cand.q + 1) # type/*
    elif cand.type == "*" and cand.subtype == "*":
      score = max(score, 1000 * cand.q) # */*
  assign best score
choose variant with highest score or 406 if all scores 0

I proceed in a similar way with Accept-Language: „de-CH“ prioritizes „de-CH“ over „de“, only then does the choice fall on the global default. I keep the selection deterministic so that caches store reliable objects.

Framework examples: Express/Node and Go

In application frameworks, I encapsulate the rules in middleware, set Vary consistently and keep fallbacks centralized.

// Express/Node (vereinfacht)
const vary = require('vary');

function negotiate(req, res, next) {
  vary(res, 'Accept, Accept-Language, Accept-Encoding');

  const types = req.accepts(['json', 'html']);
  const lang = req.acceptsLanguages(['de', 'en']) || 'de';
  res.set('Content-Language', lang);

  if (!types) return res.status(406).send('Not Acceptable');

  if (types === 'json') {
    res.type('application/json; charset=utf-8');
    return res.json({ ok: true, lang });
  }
  res.type('text/html; charset=utf-8');
  res.send(`<html lang="${lang}">OK</html>`);
}

app.get('/resource', negotiate);
// Go net/http (simplified)
func negotiateJSON(r *http.Request) bool {
  a := r.Header.Get("Accept")
  if a == "" || strings.Contains(a, "*/*") { return true }
  if strings.Contains(strings.ToLower(a), "application/json") { return true }
  return false
}

func handler(w http.ResponseWriter, r *http.Request) {
  w.Header().Add("Vary", "Accept, Accept-Language, Accept-Encoding")

  if !negotiateJSON(r) {
    w.WriteHeader(http.StatusNotAcceptable)
    w.Write([]byte("Not Acceptable"))
    return
  }
  w.Header().Set("Content-Type", "application/json; charset=utf-8")
  io.WriteString(w, `{"ok":true}`)
}

It is important that I never rely on User-Agent, but only on explicit Accept* headers. This makes behavior reproducible and testable.

Internationalization in detail

I set up a clear fallback chain, e.g. de-CH → de-DE → de → Default. If a region code does not exist, I break it down to the base language. In the response, I use Content-Language to indicate exactly the selected variant and avoid mixed forms. User preferences (such as account locale) take precedence over Accept-Language, but are also deterministically mapped to languages that the system actually provides. For SEO and accessibility, I make sure that lang attributes in the HTML and content language are consistent; I also prevent redirect loops by making the decision on the server side and instructing caches correctly via Vary.

Cleanly canonicalize request-driven variants

If I combine URL parameters (e.g. ?format=json) with Accept, the page needs a clear canonicalization: Either I accept the parameter as a hard default and ignore Accept, or the parameter is just a hint that can be overridden by Accept. I clearly document the rule and set consistent headers in the response so that caches do not store two different representations of the same URL without a separating Vary key. For HTML pages, I also ensure a unique canonical address per language/format variant within the system so that analyses and monitoring do not count duplicates.

Fine-tune and pre-produce compression

For dynamic responses, I balance the CPU costs of compression against the network savings. Brotli at level 4-6 typically provides a good ratio; higher levels are especially worthwhile for static assets that I compress in advance. I keep both br and gzip on hand for large files because not all clients support Brotli. In practice, I save the precompiles with file extensions (.br/.gz), let the server decide based on accept encoding and file size thresholds and set content encoding correctly. Important: Each compressed variant gets its own ETag; otherwise conditional requests will deliver incorrect 304 responses.

Observability, canary and rollback

I introduce negotiation rules with feature flags, activate them step by step (e.g. 5 %, 25 %, 100 %) and monitor key figures per variant: error rate, latency, bytes out, cache hit rate, proportion 406/415. I note the selected variant and the triggering headers (aggregated) in the logs so that I can quickly find mismatches. For tests, I use synthetic testers that regularly run known accept combinations against staging and production. In the event of anomalies, I specifically roll back variants without stopping the entire system - for example, by temporarily deactivating AVIF, forcing JSON as the default or reducing the Vary dimension until the cache recovers.

Summary: The right response format pays off

I deliver faster, save Bandwidth and increase satisfaction if I use content negotiation consistently. The combination of Accept headers, clear fallbacks, q-values and Vary ensures stable, reproducible responses. In practice, I prioritize server-driven decisions, keep caches variant-capable and test every rule with curl. APIs are given a strict whitelist, websites benefit from language and image variants as well as modern compression. In this way, the project achieves measurable advantages in performance, accessibility and maintainability - with a setup that I control in a targeted manner and can track at any time.

Current articles