Content negotiation in practice
Content negotiation emerges as a pivotal mechanism for elevating the accessibility of web APIs. This powerful approach empowers API integrators with the flexibility to adopt the most fitting data representation while concurrently enabling API providers to cater to a diverse range of integration needs. The process follows standardized procedures and is extensively outlined in RFC-9110, ensuring a solid foundation for implementation.
This article is split into two parts; the first one is focused on the theory behind Content Negotiation, so it will explore the patterns and mechanisms for content negotiation. The second part is focused on real life examples of content negotiation with the Spring Boot framework.
The code implemented here is available under the GitHub repository content-negotiation-example.
The theory behind Content Negotiation
As previously mentioned, the purpose of content negotiation is to establish a mutually agreeable format for request and response content between the server and the user agent. This process is standardized under RFC-9110, which highlights three distinct patterns.
Two patterns for response content
- Proactive content negotiation (server driven negotiation) In this case, the server is responsible for selecting the most convenient representation based on knowledge about the user agent. This knowledge may include explicit content negotiation headers (Accept, Accept-Encoding, Accept-Charset, Accept-Language) or implicit characteristics such as parts of theUser-Agent header or network address.
- Reactive content negotiation (agent driven negotiation) With this approach, the user agent selects the best fitting representation after receiving an initial response. Instead of sending an initial representation, the server may send a list of available representations with response code 300 (multiple choices) or 406 (not acceptable). Based on this information, the user agent performs another request with the selected representation.
One pattern for request content
- Request content negotiation The server may indicate the preferred format for request content. In such a case, the server may attach negotiation headers to its response. The user agent can reuse such headers in subsequent requests.
Content negotiation headers
We can identify the following headers within the content negotiation standard:
- Accept: The header can be used to specify preferences of message content media type. When sending in a request, it specifies user agent preferences to response content. When sending an int response, it specifies server preferences to request content in subsequent requests.
- Accept-Charset: According to RFC guidelines, has been deprecated and should be avoided if possible, however, before its deprecation, it was used to indicate preferences for charset. Wildcard can be used to indicate every charset that is not mentioned elsewhere in the field.
- Accept-Encoding: This header serves the purpose of expressing preferences for content coding. This header allows user agents to indicate the types of content coding they find acceptable in the response while also enabling servers to specify the preferred content coding for subsequent requests. An identity token is used as a synonym for no encoding.
- Accept-Language: enables user agents to express preferred natural languages for responses. It uses language tags to represent these preferences, allowing users to prioritize languages. Language tags can have associated quality values reflecting user preferences.
- Vary: is the response header that describes which parts of a request, other than the method and target, affect how the server picks the response representation. It can be a wildcard * or a list of request field names that might have played a role in picking the response representation.
Let’s start this part with a quick recap of rules that one should follow when using or handling content negotiation headers.
General rules
- When the content negotiation header doesn’t appear in request, thesender has no preferences with this dimension of negotiation.
- Within each content negotiation field, multiple preferences can be passed.
- The list of preferences can be ordered by quality values
“q”
in range0..1
, with 3 digit precision after a floating point, where 1 means most preferred,0.001
least preferred, 0 not accepted. - Quality values are placed after the given preference and split by a semicolon (e.g.,
application/json;q=0.8
) - If the quality value is not provided, it defaults to 1
- In most of content negotiation headers, where indicated wildcard may denote unspecified value (e.g.
Accept: text/*
denotes any text mime type).
Implementing media type selection via content negotiation
To illustrate how content negotiation can work, let’s implement a simple application for returning exchange rates step by step. We will implement it using Kotlin and the latest Spring Boot version.
Project generation
First, we will generate a project with spring initializr.
Let’s configure it as follows
- Gradle project with Kotlin DSL
- Kotlin as a language
- Latest stable Spring Boot version
- Java version 17
- Dependency on Spring Web
Stub data
To illustrate how content negotiation works, it would be sufficient to create a service that returns dummy data
private val EUR = Currency.getInstance("EUR")
private val USD = Currency.getInstance("USD")
private val PLN = Currency.getInstance("PLN")
@Service
class ExchangeRatesService {
private val exchangeRates: ExchangeRates = ExchangeRates(
listOf(
ExchangeRate(
baseCurrency = EUR,
quotedCurrency = USD,
rate = BigDecimal.valueOf(1.10)
),
ExchangeRate(
baseCurrency = USD,
quotedCurrency = EUR,
rate = BigDecimal.valueOf(0.90)
)
),
Instant.now()
)
fun getAllExchangeRates(): ExchangeRates {
return exchangeRates
}
}
data class ExchangeRates(val exchangeRates: List<ExchangeRate>, val updatedAt: Instant)
data class ExchangeRate(val baseCurrency: Currency, val quotedCurrency: Currency, val rate: BigDecimal)
Exposing an Endpoint with the Initial Resource Representation in application/json
Once we have it, we can proceed to implement an endpoint with the first representation (application/json
) of the response:
package com.example.contentnegotiation.controller
import com.example.contentnegotiation.domain.ExchangeRatesService
import org.springframework.http.MediaType
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
@RestController
@RequestMapping("/exchange-rates")
class ExchangeRatesController(private val exchangeRatesService: ExchangeRatesService) {
@GetMapping(produces = [MediaType.APPLICATION_JSON_VALUE])
fun getAllExchangeRates(): ExchangeRatesResponse {
val exchangeRates = exchangeRatesService.getAllExchangeRates()
return ExchangeRatesResponse(
exchangeRates = exchangeRates.exchangeRates.map {
ExchangeRateResponseItem(
baseCurrency = it.baseCurrency,
quotedCurrency = it.quotedCurrency,
rate = it.rate
)
},
updatedAt = exchangeRates.updatedAt
)
}
}
Let’s run the application and perform a test request
curl --location 'http://localhost:8080/exchange-rates'
We will receive a response:
HTTP/1.1 200
Content-Type: application/json
Transfer-Encoding: chunked
Date: Thu, 17 Aug 2023 09:38:43 GMT
{
"exchangeRates": [
{
"baseCurrency": "EUR",
"quotedCurrency": "USD",
"rate": 1.1
},
{
"baseCurrency": "USD",
"quotedCurrency": "EUR",
"rate": 0.9
}
],
"updatedAt": "2023-08-17T09:32:21.407251Z"
}
Adding a Second Representation of the Resource in application/xml
To enable XML serialization, we need to add an additional dependency com.fasterxml.jackson.dataformat:jackson-dataformat-xml
to the Gradle file.
After that, we need to extend the current endpoint and add an additional media type (MediaType.APPLICATION_XML_VALUE) in the ‘produces’ section.
@RestController
@RequestMapping("/exchange-rates")
class ExchangeRatesController(private val exchangeRatesService: ExchangeRatesService) {
@GetMapping(produces = [MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE])
fun getAllExchangeRates(): ExchangeRatesResponse {
...
Then, we can rerun the application and check if the XML representation looks proper:
curl -i --location 'http://localhost:8080/exchange-rates' \
--header 'Accept: application/xml'
We should receive a response
HTTP/1.1 200
Content-Type: application/xml
Transfer-Encoding: chunked
Date: Thu, 17 Aug 2023 10:12:28 GMT
<ExchangeRatesResponse>
<exchangeRates>
<exchangeRates>
<baseCurrency>EUR</baseCurrency>
<quotedCurrency>USD</quotedCurrency>
<rate>1.1</rate>
</exchangeRates>
<exchangeRates>
<baseCurrency>USD</baseCurrency>
<quotedCurrency>EUR</quotedCurrency>
<rate>0.9</rate>
</exchangeRates>
</exchangeRates>
<updatedAt>2023-08-17T10:12:23.792904Z</updatedAt>
</ExchangeRatesResponse>
Add a more sophisticated representation of the resource: text/html
In this step, I would like to show how we can handle a more complex representation - the one that cannot be automatically serialized. Let’s consider the text/html
format. To build the HTML content, we will utilize the Kotlin library that provides a DSL for this purpose: org.jetbrains.kotlinx:kotlinx-html-jvm
.
Now, we can implement a mapper for building the HTML representation of the exchange rates resource
package com.example.contentnegotiation.controller
import com.example.contentnegotiation.domain.ExchangeRates
import kotlinx.html.*
import kotlinx.html.stream.appendHTML
import org.springframework.context.i18n.LocaleContextHolder
import org.springframework.stereotype.Component
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
private val DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss")
@Component
class HTMLMapper {
fun mapToHTML(exchangeRates: ExchangeRates): String {
val locale = LocaleContextHolder.getLocale()
return buildString {
appendHTML().html {
head {
title { +"Exchange rates" }
style {
unsafe {
+"""
h1, h2, p {
text-align: center;
}
table {
margin-left:auto;
margin-right:auto;
}
th, td {
text-align: center;
padding: 10px;
border:1px solid black;
}
""".trimIndent()
}
}
}
body {
h1 { +"Exchange rates" }
p {
h2 {
+"Updated at: ${DATE_TIME_FORMATTER.format(exchangeRates.updatedAt.atOffset(ZoneOffset.UTC))}"
}
table {
thead {
tr {
th { +"Base currency" }
th { +"Quoted currency" }
th { +"Rate" }
}
}
tbody {
exchangeRates.exchangeRates.map {
tr {
td { +"${it.baseCurrency}" }
td { +"${it.quotedCurrency}" }
td { +"${it.rate}" }
}
}
}
}
}
}
}
}
}
}
Before reusing it in the ExchangeRatesControler, we have to add a new version of our endpoint in the controller
package com.example.contentnegotiation.controller
import com.example.contentnegotiation.domain.ExchangeRatesService
import org.springframework.http.MediaType
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
@RestController
@RequestMapping("/exchange-rates")
class ExchangeRatesController(
private val exchangeRatesService: ExchangeRatesService,
private val htmlMapper: HTMLMapper
) {
@GetMapping(produces = [MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE])
fun getAllExchangeRates(): ExchangeRatesResponse {
val exchangeRates = exchangeRatesService.getAllExchangeRates()
return ExchangeRatesResponse(
exchangeRates = exchangeRates.exchangeRates.map {
ExchangeRateResponseItem(
baseCurrency = it.baseCurrency,
quotedCurrency = it.quotedCurrency,
rate = it.rate
)
},
updatedAt = exchangeRates.updatedAt
)
}
@GetMapping(produces = [MediaType.TEXT_HTML_VALUE])
fun helloHTML(): String {
return htmlMapper.mapToHTML(exchangeRatesService.getAllExchangeRates())
}
}
Then, we can restart the application and check if the HTML representation was received properly
curl -i --location 'http://localhost:8080/exchange-rates' \
--header 'Accept: text/html'
We should receive
HTTP/1.1 200
Content-Type: text/html;charset=UTF-8
Content-Length: 1156
Date: Thu, 17 Aug 2023 11:56:35 GMT
<html>
<head>
<title>Exchange rates</title>
<style>h1, h2, p {
text-align: center;
}
table {
margin-left:auto;
margin-right:auto;
}
th, td {
text-align: center;
padding: 10px;
border:1px solid black;
}</style>
</head>
<body>
<h1>Exchange rates</h1>
<p>
<h2>Updated at: 2023-08-17 13:03:04</h2>
<table>
<thead>
<tr>
<th>Base currency</th>
<th>Quoted currency</th>
<th>Rate</th>
</tr>
</thead>
<tbody>
<tr>
<td>EUR</td>
<td>USD</td>
<td>1.1</td>
</tr>
<tr>
<td>USD</td>
<td>EUR</td>
<td>0.9</td>
</tr>
</tbody>
</table>
</p>
</body>
</html>
Evaluating the Spring Boot Content Negotiation Algorithm
We have already implemented support for three different media types. Now, we can conduct a few experiments to observe how Spring Boot handles content negotiation to select the most appropriate media type.
Support for quality values
Firstly, let’s check whether Spring Boot supports “q” values as described in RFC-9110. To do this, we will perform a few requests.
For the request
curl -i --location 'http://localhost:8080/exchange-rates' \
--header 'Accept:text/html;q=0.9,application/xml;q=0.8,application/json;q=0.7'
We are receiving
HTTP/1.1 200
Content-Type: text/html;charset=UTF-8
Content-Length: 1225
Date: Thu, 17 Aug 2023 13:13:40 GMT
...
For the request
curl -i --location 'http://localhost:8080/exchange-rates' \
--header 'Accept:text/html;q=0.8,application/xml;q=0.9,application/json;q=0.7'
We are receiving
HTTP/1.1 200
Content-Type: application/xml
Transfer-Encoding: chunked
Date: Thu, 17 Aug 2023 13:17:58 GMT
...
For the request
curl -i --location 'http://localhost:8080/exchange-rates' \
--header 'Accept:text/html;q=0.7,application/xml;q=0.8,application/json;q=0.9'
We are receiving
HTTP/1.1 200
Content-Type: application/json
Transfer-Encoding: chunked
Date: Thu, 17 Aug 2023 13:20:04 GMT
...
As we can see, in every case, Spring Boot handled the 'q' value properly, and in every case, the supported representation with the highest 'q' value was chosen.
Missing Accept header
Now, let’s explore what will happen if we don’t provide an ‘Accept’ header or if we provide it with an empty value.
For missing Accept
header value, empty value or */*
, we will receive following response:
HTTP/1.1 200
Content-Type: application/json
Transfer-Encoding: chunked
Date: Thu, 17 Aug 2023 13:20:04 GMT
...
What is the resolution algorithm in such a case?
If we delve into the Spring code, it becomes evident that a missing or empty Accept
header is defaulted with */*
. This results in three potential representations of the resource, all with the same priority. When we have multiple media types with equal quality values, the compareTo
method from the MimeType
class determines the order. In short, the alphabetical order of mime types is the deciding factor.
Unsupported value in Accept header
Let’s check what will happen when we provide an Accept
header with a value that is unsupported by the server:
For the value text/plain, we are receiving:
HTTP/1.1 406
Accept: application/json, application/xml, text/html
Content-Length: 0
Date: Thu, 17 Aug 2023 13:49:31 GMT
As we can see, the response has a 406 (Not Acceptable)
status code and contains the Accept
header with all supported media types (application/json, application/xml, text/html)
.
Fetching exchange rates from a Web browser
Interestingly, when we fetch the exchange rates resource from a web browser, we receive the HTML representation
It happens because web browsers send their preferences regarding retrieved content. As we can see, text/plain
is the first choice
Implementing Internationalization via Content Negotiation
Let's introduce support for multiple languages to showcase how content language negotiation functions in Spring... Our HTML content is an ideal candidate for internationalization.
To accomplish this, we will utilize the Spring class MessageSource
, which provides the capability to resolve text based on the locale context of the given request. Let's proceed to modify the HTMLMapper
to begin using MessageSource
:
private const val EXCHANGE_RATES = "exchange-rates"
private const val UPDATED_AT = "updated-at"
private const val BASE_CURRENCY = "base-currency"
private const val QUOTED_CURRENCY = "quoted-currency"
private const val RATE = "rate"
private val DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss")
@Component
class HTMLMapper(private val messageSource: MessageSource) {
fun mapToHTML(exchangeRates: ExchangeRates): String {
val locale = LocaleContextHolder.getLocale()
return buildString {
appendHTML().html {
head {
title { +messageSource.getMessage(EXCHANGE_RATES, null, locale)}
...
table {
thead {
tr {
th { +messageSource.getMessage(BASE_CURRENCY, null, locale) }
th { +messageSource.getMessage(QUOTED_CURRENCY, null, locale) }
th { +messageSource.getMessage(RATE, null, locale)}
}
}
...
In the next step, let’s prepare files with translations. By default, configuration files from the resource bundle named with the convention messages_{language_code}.properties
will be used. The default language should be placed in the file messages.properties
.
Now, let’s add translations for three languages (English, German, Polish), with English set as the default
All language files have the following properties with proper translation:
messages.properties
exchange-rates=Exchange rates
updated-at=Updated at
base-currency=Base currency
quoted-currency=Quoted currency
rate=Rate
In the next step, we need to configure the list of supported locales. This can be achieved by providing a LocaleResolver
bean
@Configuration
class LocaleConfiguration {
@Bean
fun localeResolver(): LocaleResolver {
val localeResolver = AcceptHeaderLocaleResolver()
localeResolver.setDefaultLocale(Locale.ENGLISH)
localeResolver.supportedLocales = listOf(Locale.ENGLISH, Locale.GERMANY, Locale("pl"))
return localeResolver
}
}
After completing this step, we can rerun the application and make a few requests
Missing Accept-Language header
Request without Accept-Language header
curl -i --location 'http://localhost:8080/exchange-rates' \
--header 'Accept: text/html'
gives response in the default language, which is English
HTTP/1.1 200
Content-Type: text/html;charset=UTF-8
Content-Length: 1225
Date: Fri, 18 Aug 2023 09:35:31 GMT
<html>
<head>
<title>Exchange rates</title>
<style>h1, h2, p {
text-align: center;
}
table {
margin-left:auto;
margin-right:auto;
}
th, td {
text-align: center;
padding: 10px;
border:1px solid black;
}</style>
</head>
<body>
<h1>Exchange rates</h1>
<p>
<h2>Updated at: 2023-08-18 09:27:59</h2>
<table>
<thead>
<tr>
<th>Base currency</th>
<th>Quoted currency</th>
<th>Rate</th>
</tr>
...
Accept-Language with unsupported value
When we provide an unsupported Spanish language in the Accept-Language
header
curl -i --location 'http://localhost:8080/exchange-rates' \
--header 'Accept: text/html' \
--header 'Accept-Language: es'
The response will be returned in the default language (unlike the Accept header, a 406 response won’t be returned in this case)
HTTP/1.1 200
Content-Type: text/html;charset=UTF-8
Content-Length: 1225
Date: Fri, 18 Aug 2023 09:35:31 GMT
<html>
<head>
<title>Exchange rates</title>
<style>.....</style>
</head>
<body>
<h1>Exchange rates</h1>
<p>
<h2>Updated at: 2023-08-18 09:27:59</h2>
<table>
<thead>
<tr>
<th>Base currency</th>
<th>Quoted currency</th>
<th>Rate</th>
</tr>
...
List of languages with quality values
When we make a request and provide a list of languages with q values
curl -i --location 'http://localhost:8080/exchange-rates' \
--header 'Accept: text/html' \
--header 'Accept-Language: en;q=0.5,pl;q=0.7,de;q=0.6,es;0.9'
We will receive the supported language with the highest q value. In this case, it would be Polish
HTTP/1.1 200
Content-Type: text/html;charset=UTF-8
Content-Length: 1225
Date: Fri, 18 Aug 2023 09:42:44 GMT
<html>
<head>
<title>Kursy wymian</title>
<style>h1, h2, p {
text-align: center;
}
table {
margin-left:auto;
margin-right:auto;
}
th, td {
text-align: center;
padding: 10px;
border:1px solid black;
}</style>
</head>
<body>
<h1>Kursy wymian</h1>
<p>
<h2>Zaktualizowane: 2023-08-18 09:27:59</h2>
<table>
<thead>
<tr>
<th>Bazowa waluta</th>
<th>Notowana waluta</th>
<th>Kurs</th>
</tr>
...
Fetching exchange rates from a Web browser
Similar to the case with content type, the browser also sends its preferences regarding content language
Preferences can be configured in the browser settings. In the case of Google Chrome, it appears as follows
In my case, the default language is set to Polish, so I received content in that language
Negotiation of Content Encoding
To enable content encoding, we need to configure three Spring Boot properties
server.compression.enabled
: for enabling content compression on the server sideserver.compression.mime-types
: to specify the content types for which compression can be appliedserver.compression.min-response-size
: the minimum size of the response to be compressed, measured in bytes.
Let’s add these properties to the application.properties
file
server.compression.enabled=true
server.compression.mime-types=text/html,application/json,application/xml
server.compression.min-response-size=1024
Now, we can restart the application and perform a few test requests
Request without Accept-Encoding
When we initiate a request
curl -sI --location 'http://localhost:8080/exchange-rates' \
--header 'Accept: text/html'
We will receive the following response
HTTP/1.1 200
vary: accept-encoding
Content-Type: text/html;charset=UTF-8
Content-Length: 1225
Date: Fri, 18 Aug 2023 12:42:37 GMT
As we can see, the response was not compressed, even though the response size exceeds the minimum compression size that we have set. Another aspect we can observe is the presence of the Vary: Accept-Encoding
header, which indicates that the Accept-Encoding
header can be taken into consideration during response creation.
Request with indicated content encoding
When we make a request and indicate gzip
as an acceptable form of encoding
curl -sI --location 'http://localhost:8080/exchange-rates' \
--header 'Accept: text/html' \
--header 'Accept-Encoding: gzip'
We will receive a compressed response
HTTP/1.1 200
vary: accept-encoding
Content-Encoding: gzip
Content-Type: text/html;charset=UTF-8
Transfer-Encoding: chunked
Date: Fri, 18 Aug 2023 13:25:33 GMT
Quality value used in content encoding
When we initiate a request and include two values in the Accept-Encoding
header, gzip
and identity
, where the identity
has a higher quality value:
curl -sI --location 'http://localhost:8080/exchange-rates' \
--header 'Accept: text/html' \
--header 'Accept-Encoding: gzip;q=0.1,identity;q=0.3'
Then, we will receive a compressed response
HTTP/1.1 200
vary: accept-encoding
Content-Encoding: gzip
Content-Type: text/html;charset=UTF-8
Transfer-Encoding: chunked
Date: Fri, 18 Aug 2023 13:32:52 GMT
As we can see, prioritizing based on the higher ‘q’ value is not observed in this case.
Encoding preference with a quality value equal to zero
When we initiate a request
curl -sI --location 'http://localhost:8080/exchange-rates' \
--header 'Accept: text/html' \
--header 'Accept-Encoding: gzip;q=0'
We will receive a response without compression
HTTP/1.1 200
vary: accept-encoding
Content-Type: text/html;charset=UTF-8
Content-Length: 1225
Date: Fri, 18 Aug 2023 13:36:40 GMT
As we can see, the quality value can be used to exclude selected compression formats.
Adjustments of Vary header
As observed, the Vary
header was added to responses once we enabled response compression. However, this behavior doesn’t entirely align with our intentions. We’ve implemented content negotiation based on headers like Accept, Accept-Language
, and Accept-Encoding
, all of which can alter the response representation.
Unfortunately, Spring doesn’t provide a specific property to modify this behavior. However, we can implement a simple workaround using a servlet filter:
@WebFilter("/exchange-rates")
class ExchangeRateFilter : Filter{
override fun doFilter(request: ServletRequest?, response: ServletResponse?, chain: FilterChain?) {
val httpServletResponse = response as HttpServletResponse
httpServletResponse.setHeader(HttpHeaders.VARY, "${HttpHeaders.ACCEPT}, ${HttpHeaders.ACCEPT_LANGUAGE}, ${HttpHeaders.ACCEPT_ENCODING}")
chain!!.doFilter(request, response)
}
}
In order to make this work, we need to add the annotation @ServletComponentScan
as well.
@SpringBootApplication
@ServletComponentScan
class ContentNegotiationApplication
fun main(args: Array<String>) {
runApplication<ContentNegotiationApplication>(*args)
}
In the next step, we can restart the application and proceed with a sample request
curl -I --location 'http://localhost:8080/exchange-rates'
In the response, we should receive the Vary header containing all the fields that can alter the response representation
HTTP/1.1 200
Vary: accept,accept-language,accept-encoding
Content-Type: application/json
Transfer-Encoding: chunked
Date: Fri, 18 Aug 2023 14:31:51 GMT
Summary
In this article, we delved into the theory of content negotiation as outlined in RFC-9110. After exploring the theoretical aspects, we proceeded to implement a practical example using Spring Boot.
Below I am attaching a summary of Spring Boot support for content negotiation, how it is compliant with theory and how it behaves in edge cases:
Negotiation element | Header | Consideration of q value | Handling for missing header | Values in the header are unsupported | Considered in Vary header | Content negotiation pattern |
---|---|---|---|---|---|---|
Media type | Accept | Yes | Supported media type, which is first in alphabetical order, is returned | 406 (Not acceptable) is returned; Accept header is attached to the response with the list of supported media types | No | Proactive, Reactive |
Language | Accept-Language | Yes | The default language is selected | The default language is selected | No | Proactive |
Encoding | Accept-Encoding | No | No encoding is applied | No encoding is applied | Yes | Proactive |
Notably, Spring Boot offers a built-in content negotiation mechanism that is highly versatile. This mechanism can be seen as a form of proactive content negotiation, where a 406 response code is only received in rare cases when the server cannot provide a satisfactory representation.
However, it’s worth noting that its strict adherence to the RFC-9110 specification might not be absolute in certain scenarios. This was particularly evident in the negotiation of encodings and the attachment of the Vary header, which required our adjustment.
Reviewed by: Bartłomiej Żyliński, Tomasz Dziurko