Contents

Content negotiation in practice

Łukasz Rola

02 Nov 2023.16 minutes read

Content negotiation in practice webp image

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

  1. When the content negotiation header doesn’t appear in request, thesender has no preferences with this dimension of negotiation.
  2. Within each content negotiation field, multiple preferences can be passed.
  3. The list of preferences can be ordered by quality values “q” in range 0..1, with 3 digit precision after a floating point, where 1 means most preferred, 0.001 least preferred, 0 not accepted.
  4. Quality values are placed after the given preference and split by a semicolon (e.g., application/json;q=0.8)
  5. If the quality value is not provided, it defaults to 1
  6. In most of content negotiation headers, where indicated wildcard may denote unspecified value (e.g. Accept: text/* denotes any text mime type).

Content%20Negotiation%20in%20Practice

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

image6

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
image5

It happens because web browsers send their preferences regarding retrieved content. As we can see, text/plain is the first choice
image7

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

image3

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

image4

Preferences can be configured in the browser settings. In the case of Google Chrome, it appears as follows

image1

In my case, the default language is set to Polish, so I received content in that language

image2

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 side
  • server.compression.mime-types: to specify the content types for which compression can be applied
  • server.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 elementHeaderConsideration of q valueHandling for missing headerValues in the header are unsupportedConsidered in Vary headerContent negotiation pattern
Media typeAcceptYesSupported media type, which is first in alphabetical order, is returned406 (Not acceptable) is returned; Accept header is attached to the response with the list of supported media typesNoProactive, Reactive
LanguageAccept-LanguageYesThe default language is selectedThe default language is selectedNoProactive
EncodingAccept-EncodingNoNo encoding is appliedNo encoding is appliedYesProactive

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

Blog Comments powered by Disqus.