Walk through how Spring resolves the exact response type when the Accept header lists several media types with q-values and the server has multiple converters.
answer
- Sort Accept by q then specificity
- Producible = converters ∩ produces
- isCompatibleWith + most-specific wins
- Empty intersection → 406
- Textual order ≠ priority; q-value rules
basics
~20 sSpring parses Accept into media types sorted by q-value and specificity, computes the types the converters (and any produces) can write, then intersects the two lists and picks the most specific, highest-priority match. If the intersection is empty it returns 406.
solid answer
~40 s`HeaderContentNegotiationStrategy` parses `Accept` into a list of `MediaType`s and sorts them by **quality (q-value)** then **specificity** (`MediaType.sortBySpecificityAndQuality`). Separately, `AbstractMessageConverterMethodProcessor` builds the **producible** media types: for every `HttpMessageConverter` whose `canWrite` matches the return type, its supported types, intersected with the mapping's `produces` if present. It then iterates acceptable types (in priority order) and, for each, finds a compatible producible type using `MediaType.isCompatibleWith`, choosing the more concrete of the two (so `application/*` acceptable + `application/json` producible → JSON). The first compatible pair wins; that converter serializes and sets Content-Type. If no acceptable type is compatible with any producible type, it throws `HttpMediaTypeNotAcceptableException` → **406**. `*/*` matches the first producible type.
code
java · 19 lines// Given: Jackson (JSON) + Jackson-XML converters both registered.
// Request: Accept: application/json;q=0.5, application/xml;q=0.9
//
// 1) Header strategy -> [application/xml (q0.9), application/json (q0.5)]
// 2) Producible from converters -> [application/json, application/xml, ...]
// 3) First compatible in priority order -> application/xml wins (higher q)
// 4) MappingJackson2XmlHttpMessageConverter writes; Content-Type: application/xml
@GetMapping("/report/{id}") // no 'produces' -> both formats producible
public Report report(@PathVariable long id) {
return service.report(id); // XML returned for the Accept above
}
// Contrast: pin the endpoint to JSON only.
@GetMapping(value = "/report2/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
public Report reportJsonOnly(@PathVariable long id) {
// Accept: application/xml here -> producible is JSON-only -> 406 Not Acceptable
return service.report(id);
}go deeper
Know Accept can list multiple types and Spring picks one it can produce.
Explain q-values override textual order and empty match → 406.
Detail the sort-then-intersect algorithm, isCompatibleWith, most-specific selection, and produces as a ceiling.
Anticipate real-world Accept headers (browsers), charset/parameter specificity edge cases, and design produces/converter sets to avoid surprising 406s.
This question drills into the *selection algorithm* — the heart of negotiation. **Step 1 — parse & sort the Accept header.** Given `Accept: application/xml;q=0.9, application/json, text/*;q=0.5`, `HeaderContentNegotiationStrategy` produces `MediaType` objects each carrying a `q` parameter (default `q=1.0`). Spring sorts them with `MediaType.sortBySpecificityAndQuality`: - Higher **q-value** first (json q=1.0 before xml q=0.9 before text/* q=0.5). - Among equal q, **more specific** first (`application/json` before `application/*` before `*/*`; a type with more parameters is more specific). Result order here: `application/json` (q1), `application/xml` (q0.9), `text/*` (q0.5). **Step 2 — compute producible types.** `AbstractMessageConverterMethodProcessor.getProducibleMediaTypes` looks at either (a) the `produces` condition stored as a request attribute (`PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE`) if the mapping declared one, or (b) every registered `HttpMessageConverter` whose `canWrite(returnType, null)` is true, collecting `getSupportedMediaTypes`. So with Jackson + JAXB converters and a serializable return type, producible ≈ `[application/json, application/xml, ...]`. **Step 3 — match acceptable against producible.** It loops the sorted acceptable types; for each acceptable `A`, loops producible types `P` and tests `A.isCompatibleWith(P)`. Compatibility treats wildcards as matching (`text/*` compatible with `text/plain`; `*/*` compatible with anything). When compatible, it selects the **more concrete** media type via `getMostSpecificMediaType` (so the abstract `application/*` in Accept resolves to the concrete `application/json` producible type — you never send a wildcard Content-Type). Matches are collected and again sorted by specificity/quality; the top one is chosen. **Step 4 — outcome.** - A winner: the corresponding converter writes the body; response `Content-Type` = the chosen concrete type (q-value stripped). - No compatible pair across all acceptable×producible: **`HttpMediaTypeNotAcceptableException`** → **406 Not Acceptable**, with an `Accept` response header advertising producible types. **Worked examples:** - `Accept: application/json` + Jackson present → JSON. ✔ - `Accept: application/xml` but only Jackson (no XML converter) → producible has no XML → **406**. - `Accept: */*` → the very first producible type (often JSON) is chosen; wildcard never appears as Content-Type. - `Accept: application/json;q=0.5, application/xml;q=0.9` with both converters → XML wins (higher q), *despite* JSON appearing first textually. - `produces = APPLICATION_JSON_VALUE` on the method + `Accept: application/xml` → producible is JSON-only → **406** even though an XML converter exists, because `produces` narrows producible. **Gotchas:** - Textual order in Accept is irrelevant; only q then specificity matter. Many candidates wrongly assume left-to-right precedence. - Browsers send broad Accept headers (`text/html,application/xhtml+xml,...,*/*;q=0.8`) — the trailing `*/*;q=0.8` is why an API often still returns JSON to a browser. - `produces` acts as a hard ceiling on producible types; over-narrow `produces` causes surprising 406s. - A missing/empty Accept is treated as `*/*` (first producible wins) unless `defaultContentType` overrides. - Charset and other parameters participate in specificity and can cause mismatches if a converter's supported type lacks the requested parameter.
- `Accept: application/json;q=0.2, application/xml;q=0.8` with both converters available — which format is returned and why?XML. Selection is by q-value, not textual order; xml's q=0.8 outranks json's q=0.2, so the XML converter wins.
- What response header does Spring add on a 406 to help the client?It sets the `Accept` response header listing the media types the server *can* produce for that endpoint, so the client can retry with a supported type.
saying these in an interview costs you the question
- Assuming the first media type listed in Accept always wins regardless of q-value.
- Thinking a wildcard like application/* could be sent back as the literal Content-Type.
- Believing an XML converter on the classpath guarantees XML output even when produces pins the method to JSON.