🌐 Розділ 6 · Питання #17

Що таке 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 однакові, пріоритет віддається більш специфічному типу:

  1. application/vnd.company.user+json (найвищий пріоритет — точний вендорний тип)
  2. application/json (конкретний підтип)
  3. application/* (усі підтипи групи)
  4. */* (будь-який тип)

Кешування та заголовок 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 каверзних питання з відповідями

  1. Який HTTP-код має повернути сервер, якщо клієнт надіслав Accept: text/csv, а контролер уміє віддавати лише application/json? Відповідь: Сервер зобов’язаний повернути статус 406 Not Acceptable. Повернення помилки 400 або 404 у цьому випадку є порушенням специфікації HTTP.

  2. Навіщо потрібен заголовок Vary: Accept під час реалізації Content Negotiation? Відповідь: Без Vary: Accept сервери кешування (CDN, проксі) кешують відповідь виключно за URL. Якщо перший клієнт запросив JSON, а другий XML за тією самою адресою, другий клієнт отримає з кешу тіло у форматі JSON. Заголовок Vary: Accept змушує кеш зберігати окремі записи кешу для кожного значення заголовка Accept.

  3. Що означає заголовок 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.

  4. У чому різниця між заголовками 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).

Пов’язані теми