Negotiation selects among representations the server can actually produce. Request preferences are inputs to that decision, not evidence that the response will satisfy them. A client still needs to inspect the selected representation. Vary records the request dimensions relevant to reuse; it does not identify the chosen format, language, or compression method.

Keep the dimensions separate

Request preferenceSelected representation metadata
AcceptContent-Type
Accept-LanguageContent-Language
Accept-EncodingContent-Encoding, when a content coding is applied

These dimensions are independent. Requesting JSON does not request English or gzip. Content-Language describes the intended audience’s language, not a guarantee that every character belongs to one language. A server offering English HTML and Chinese JSON cannot combine the independently preferred format and language into Chinese HTML unless that complete representation exists. Define the available combinations before deciding how to rank them.

A quality value ranges from zero to one, defaults to one, and permits at most three fractional digits. Zero means unacceptable, not merely less desirable. For media types, the most specific matching range determines a candidate’s quality. An explicit text/html;q=0 is not rescued by */*;q=1. Looking for the largest number anywhere in the field, or choosing the first comma-separated item, loses this relationship between range and candidate.

When it breaks

A missing preference field generally expresses no preference on that dimension. It is not the same input as an explicit exclusion. RFC 9110 permits a server with no acceptable representation to send 406 or disregard the field; a default response might therefore be unusable to the client. An API promising strict negotiation must define that policy itself. An exclusion communicates a constraint but does not force every deployed server to enforce it.

Different fields also have different matching rules. Accept uses media ranges; language negotiation needs a documented language-tag matching scheme; Accept-Encoding has special rules for identity, absence, and an empty value. A generic list parser cannot supply all those policies. Do not silently equate an absent field with an empty parsed collection, then conclude that the client rejected everything.

Equal weights do not establish a universal ordering rule across all servers. Make the application’s tie policy deterministic, and test it. Detailed language preferences can also expose identifying information, so neither logging complete preference profiles nor adding every possible field to Vary is automatically justified. Vary affects matching, while storage eligibility, freshness, and authorization still require their own rules.

Check it quickly

Use Python 3.11 or newer and a POSIX shell. Install the tested parser in a temporary environment, then run the Python block with "$accept_lab/venv/bin/python". Installation needs the network; the experiment itself does not send requests or start a server. It checks selection behavior in Werkzeug 3.1.8 rather than claiming to reproduce an arbitrary browser or CDN.

accept_lab=$(mktemp -d)
python3 -m venv "$accept_lab/venv"
"$accept_lab/venv/bin/python" -m pip install Werkzeug==3.1.8
from werkzeug.datastructures import Accept, LanguageAccept, MIMEAccept
from werkzeug.http import parse_accept_header

offers = ["text/html", "application/json"]
cases = [
    (None, "text/html"),
    ("application/json;q=0.9, text/html;q=0.2", "application/json"),
    ("text/*;q=0.9, text/html;q=0, application/json;q=0.5", "application/json"),
    ("application/json;q=0, text/*;q=0.4, */*;q=1", "text/html"),
    ("application/xml", None),
    ("*/*;q=0", None),
]
for header, expected in cases:
    parsed = parse_accept_header(header, MIMEAccept)
    selected = offers[0] if header is None else parsed.best_match(offers)
    assert selected == expected
    print(repr(header), "->", selected)

denied = parse_accept_header("text/html;q=0, */*;q=1", MIMEAccept)
assert denied.quality("text/html") == 0
assert denied.quality("application/json") == 1
print("specific exclusion:", denied.quality("text/html"), denied.quality("application/json"))

tied = parse_accept_header("application/json, text/html", MIMEAccept)
assert tied.best_match(offers) == "text/html"
assert tied.best_match(list(reversed(offers))) == "application/json"
print("tie: server offer order decides")

languages = parse_accept_header("fr;q=1, zh;q=0.8, en;q=0.5", LanguageAccept)
codings = parse_accept_header("gzip;q=0, br;q=1", Accept)
assert languages.best_match(["en", "zh"]) == "zh"
assert codings.best_match(["gzip", "br"]) == "br"
print("independent axes:", languages.best_match(["en", "zh"]), codings.best_match(["gzip", "br"]))

The six media results are HTML, JSON, JSON, HTML, no match, and no match. The missing-field HTML default is explicitly chosen by the caller. The two exclusion cases prove that a broader range does not override the specific candidate’s quality. The unavailable XML and universal zero cases return None; translating that result into 406 belongs to an HTTP endpoint, not this parser.

The tie test reverses the server’s offered list while leaving the request unchanged. Werkzeug follows that list when quality and specificity tie. The final checks independently select Chinese and Brotli from declared candidates; they neither create a combined representation nor perform compression. These language cases use direct tags and do not validate regional fallback. The coding check deliberately does not implement identity or missing-header semantics.

For a real integration, preserve these negative cases and inspect the actual outgoing request, returned metadata, and body. Test the default, supported alternatives, explicit exclusions, and unavailable combinations through the deployed path. Only then decide which request fields belong in Vary. A passing parser test establishes a useful component contract, not the correctness of the complete negotiation and caching pipeline.

Sources