Що таке Accept header
У REST-архітектурі один і той самий ресурс (наприклад, користувач або звіт) може мати декілька представлень: JSON, XML, PDF, CSV. Заголовок Accept дозволяє клієнту запросити най...
🟢 Junior Level
Accept — це заголовок HTTP-запиту (RFC 9110, розділ 12.5.1), за допомогою якого клієнт повідомляє серверу, у якому форматі (Media Type / MIME Type) він очікує отримати тіло відповіді.
Принцип роботи
У REST-архітектурі один і той самий ресурс (наприклад, користувач або звіт) може мати декілька представлень: JSON, XML, PDF, CSV. Заголовок Accept дозволяє клієнту запросити найбільш прийнятне для нього представлення:
GET /api/v1/users/42 HTTP/1.1
Accept: application/json
Сервер серіалізує об’єкт у JSON та повертає відповідь:
HTTP/1.1 200 OK
Content-Type: application/json
{"id": 42, "name": "Ivan"}
Різниця між Accept та Content-Type
Accept(Запит відповіді): Клієнт оголошує: «Я хочу отримати відповідь у форматі JSON».Content-Type(Поточні дані): Відправник оголошує: «Прямо зараз у тілі цього повідомлення передаються дані у форматі JSON».
Що станеться, якщо формат не підтримується?
Якщо клієнт запросив Accept: application/xml, а сервер уміє віддавати лише JSON, сервер зобов’язаний повернути HTTP статус-код 406 Not Acceptable.
🟡 Middle Level
Механіка Content Negotiation (Узгодження контенту)
Процес вибору сервером найкращого формату відповіді на основі заголовків клієнта називається Server-Driven Content Negotiation:
Client (Accept: application/json;q=0.9, application/xml;q=0.5)
│
▼
Spring DispatcherServlet
│
▼
ContentNegotiationManager
(Аналізує список підтримуваних MediaType контролера)
│
Метод @GetMapping(produces = {JSON, XML})
│
Обирає JSON (вага q=0.9 вища)
│
▼
MappingJackson2HttpMessageConverter (canWrite -> true)
│
▼
HTTP 200 OK (Content-Type: application/json)
Вагові коефіцієнти якості (q-values)
Клієнт може передавати перелік форматів із пріоритетами від 0.0 до 1.0 (за замовчуванням q=1.0):
Accept: application/json;q=1.0, application/xml;q=0.8, text/plain;q=0.2, */*;q=0.1
- Якщо сервер підтримує і JSON, і XML — він обере JSON (
q=1.0). - Якщо JSON недоступний, сервер здійснить м’яку деградацію (Graceful Degradation) та поверне XML (
q=0.8). - Маска
*/*з низькою вагою повідомляє: «Якщо нічого з переліченого немає, віддайте будь-який формат, але не повертайте помилку 406».
Правила специфічності MIME-типів
Якщо ваги q однакові, пріоритет віддається більш специфічному типу:
application/vnd.company.user+json(найвищий пріоритет — точний вендорний тип)application/json(конкретний підтип)application/*(усі підтипи групи)*/*(будь-який тип)
Кешування та заголовок Vary: Accept (RFC 9111)
Якщо сервер віддає різні формати за одним і тим самим URL, він зобов’язаний повернути заголовок:
HTTP/1.1 200 OK
Content-Type: application/json
Vary: Accept
Навіщо це потрібно:
Проміжний кеш (CDN Cloudflare, NGINX) за замовчуванням використовує як ключ кешування лише URL (GET /users/42).
Якщо заголовок Vary: Accept відсутній, відповідь у форматі JSON закешується. Наступний клієнт (наприклад, SOAP/XML інтеграція), який запросить Accept: application/xml, отримає з кешу пошкоджений для нього JSON! Заголовок Vary інструктує кеш зберігати окремі копії відповіді для різних значень Accept.
🔴 Senior Level
Сімейство заголовків узгодження контенту (Content Negotiation Family)
Специфікація HTTP визначає чотири паралельні виміри узгодження відповіді:
| Заголовок запиту | Заголовок відповіді сервера | Що узгоджується | Помилка за невідповідності |
|---|---|---|---|
Accept |
Content-Type |
Формат даних (MIME type) | 406 Not Acceptable |
Accept-Encoding |
Content-Encoding |
Алгоритм стиснення (gzip, br, zstd) |
406 (або відповідь без стиснення) |
Accept-Language |
Content-Language |
Мова локалізації (uk-UA, en-US) |
Fallback на дефолтну мову |
Accept-Charset |
Content-Type; charset= |
Кодування тексту (застарілий, UTF-8 де-факто) | 406 Not Acceptable |
Production конфігурація Content Negotiation у Spring Boot 3
За замовчуванням Spring Boot 3 вимикає небезпечне узгодження через суфікси файлів (/users/1.json), спираючись виключно на заголовок Accept:
package com.example.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMvcContentNegotiationConfig implements WebMvcConfigurer {
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer
// 1. Вимикаємо суфікси у шляху (/api/users.json заборонено)
.favorPathExtension(false)
// 2. Вимикаємо узгодження через query параметр (/api/users?format=json)
.favorParameter(false)
// 3. Використовуємо виключно HTTP-заголовок Accept
.ignoreAcceptHeader(false)
// 4. Формат за замовчуванням, якщо заголовок відсутній або передано */*
.defaultContentType(MediaType.APPLICATION_JSON)
// 5. Явна реєстрація підтримуваних медіа-типів
.mediaType("json", MediaType.APPLICATION_JSON)
.mediaType("xml", MediaType.APPLICATION_XML)
.mediaType("csv", MediaType.parseMediaType("text/csv"));
}
}
Контролер із множинними представленнями
package com.example.controller;
import com.example.dto.ReportDto;
import com.example.service.ReportExportService;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/v1/reports")
public class ReportExportController {
private final ReportExportService exportService;
public ReportExportController(ReportExportService exportService) {
this.exportService = exportService;
}
// 1. Повернення у форматі JSON
@GetMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<ReportDto> getReportAsJson(@PathVariable Long id) {
ReportDto report = exportService.findById(id);
return ResponseEntity.ok()
.header(HttpHeaders.VARY, HttpHeaders.ACCEPT)
.body(report);
}
// 2. Повернення у форматі CSV (той самий URI, інший Accept)
@GetMapping(value = "/{id}", produces = "text/csv")
public ResponseEntity<String> getReportAsCsv(@PathVariable Long id) {
String csvData = exportService.generateCsv(id);
return ResponseEntity.ok()
.header(HttpHeaders.VARY, HttpHeaders.ACCEPT)
.contentType(MediaType.parseMediaType("text/csv; charset=utf-8"))
.body(csvData);
}
}
🎯 Шпаргалка для інтерв’ю
30-секундна відповідь
Accept(RFC 9110) — це HTTP-заголовок запиту, яким клієнт оголошує серверу перелік очікуваних форматів відповіді (MIME-типів).Підтримує коефіцієнти пріоритету (q-values від 0.0 до 1.0), дозволяючи реалізувати м’яку деградацію формату відповіді. Якщо сервер не підтримує жоден із запитаних форматів, він зобов’язаний повернути статус
406 Not Acceptable.Якщо ендпоінт віддає різні формати за одним URL, сервер зобов’язаний повертати заголовок
Vary: Accept, щоб запобігти віддачі несумісного формату з проміжного кешу (CDN / NGINX).
4 каверзних питання з відповідями
-
Який HTTP-код має повернути сервер, якщо клієнт надіслав
Accept: text/csv, а контролер уміє віддавати лишеapplication/json? Відповідь: Сервер зобов’язаний повернути статус406 Not Acceptable. Повернення помилки 400 або 404 у цьому випадку є порушенням специфікації HTTP. -
Навіщо потрібен заголовок
Vary: Acceptпід час реалізації Content Negotiation? Відповідь: БезVary: Acceptсервери кешування (CDN, проксі) кешують відповідь виключно за URL. Якщо перший клієнт запросив JSON, а другий XML за тією самою адресою, другий клієнт отримає з кешу тіло у форматі JSON. ЗаголовокVary: Acceptзмушує кеш зберігати окремі записи кешу для кожного значення заголовкаAccept. -
Що означає заголовок
Accept: text/html, application/xhtml+xml, application/xml;q=0.9, */*;q=0.8? Відповідь: Клієнт у першу чергу віддає перевагуtext/htmlабоapplication/xhtml+xmlз максимальним пріоритетомq=1.0. Якщо вони недоступні, допустимийapplication/xmlіз пріоритетом0.9. Якщо й XML не підтримується сервером, клієнт згоден на будь-який інший формат (*/*) із пріоритетом0.8. -
У чому різниця між заголовками
Accept,Accept-EncodingтаAccept-Charset? Відповідь:Acceptузгоджує формат даних та схему серіалізації (application/json,text/csv).Accept-Encodingузгоджує алгоритм стиснення корисного навантаження (gzip,br,deflate).Accept-Charsetузгоджує кодування символів тексту (на сьогодні застарів, оскільки всюди прийнято UTF-8).
Червоні прапорці (чого категорично не можна говорити)
- ❌ «Accept і Content-Type — це одне й те саме, просто в запиті та відповіді» (
Acceptзапитує формат майбутньої відповіді,Content-Typeописує формат тіла, що надсилається прямо зараз). - ❌ «406 Not Acceptable означає, що сервер упав через внутрішню помилку» (406 — це клієнтська помилка 4xx: клієнт запросив непідтримуваний формат).
- ❌ «Заголовок Vary: Accept потрібен лише для безпеки та шифрування» (він потрібен для коректного розділення кешу на CDN і проксі).
- ❌ «Content Negotiation у сучасному REST краще робити через суфікси в URL: /users.json та /users.xml» (у Spring Boot 3 суфікси шляхів визнано застарілими; стандартом є заголовок
Accept).