🌐 Раздел 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 Язык локализации (ru-RU, 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"));
    }
}

Контроллер с множественными представлениями и RFC 9457 ProblemDetail

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 в modern REST лучше делать через суффиксы в URL: /users.json и /users.xml» (в Spring Boot 3 суффиксы путей признаны устаревшими; стандартом является заголовок Accept).

Связанные темы