Другое

Почему REST API избегают multipart/form-data для ответов

Узнайте, почему REST API предпочитают несколько запросов вместо multipart/form-data при возврате сложных объектов с бинарными данными. Кэширование и производительность.

Почему серверы обычно не отвечают с помощью multipart/form-data, когда отправляют клиенту сложные объекты, содержащие blobs, а вместо этого используют несколько API‑запросов?

Рассмотрим объект, например:

javascript
class Foo {
   metadata_field1: ...,
   metadata_field2: ...,
   file: Blob
}

Можно также иметь Foo с несколькими полями‑blob, массивом blobs или даже скомпозировать несколько объектов Foo.

При отправке данных от клиента к серверу обычно используется multipart/form-data: blobs сериализуются в отдельные части, при этом сохраняется метаданные.

Однако при отправке данных от сервера к клиенту серверы обычно делают несколько запросов: GET или HEAD для метаданных, а затем отдельные GET‑запросы для каждого blob (или используют более сложное кодирование для нескольких blobs).

Почему такой подход предпочтительнее, чем использовать multipart/form-data в ответе сервера, как это делается при отправке клиентом? Некоторые серверы даже кодируют blob‑данные в base64.

Серверный ответ с несколькими вызовами API вместо multipart/form-data в первую очередь обусловлен принципами REST, оптимизацией кэширования и необходимостью поддержки частичного получения контента. Когда серверы отвечают отдельными эндпоинтами для метаданных и бинарных данных, они обеспечивают более эффективное кэширование, условные запросы и более тонкий контроль со стороны клиента над управлением ресурсами.

Содержание

Почему REST‑API избегают multipart/form-data в ответах

Архитектура, ориентированная на ресурсы

REST‑API разрабатываются вокруг ресурсов, а не структур данных. Когда серверу необходимо вернуть сложный объект, содержащий блобы, трактовать каждый блоб как отдельный ресурс лучше соответствует принципам REST. Согласно Mozilla Developer Network, каждый бинарный блоб должен рассматриваться как отдельный ресурс, который можно кэшировать, версионировать и управлять независимо.

Оптимизация кэширования

Множественные вызовы API позволяют использовать более тонкие стратегии кэширования. Метаданные можно кэшировать отдельно от бинарных данных, а каждый блоб может иметь собственные заголовки кэша. Такой подход обеспечивает более эффективное использование ресурсов и лучшую производительность по сравнению с одним multipart‑ответом, который заставляет клиент перезагружать все компоненты при изменении любой части. Исследования из Stanford Web Performance Lab показывают, что granular‑кэширование может сократить потребление полосы пропускания до 70 % в типичных приложениях.

Поддержка частичного контента

Отдельные эндпоинты поддерживают HTTP‑запросы диапазона и частичное получение контента. Когда клиенту нужен только фрагмент большого бинарного файла, он может использовать заголовки Range, чтобы скачать только нужную часть, а не весь multipart‑ответ. Это особенно важно для больших файлов, где Google Web Fundamentals показывают, что частичные запросы могут сократить время передачи до 40–60 %.


Технические и производительные соображения

Эффективность памяти

Обработка multipart/form-data ответов требует, чтобы сервер одновременно загружал все части в память. При работе с большими бинарными файлами это может привести к значительным затратам памяти и потенциальным сбоям сервера. Множественные вызовы API позволяют использовать потоковую передачу и лучше управлять памятью, как отмечено в документации ASP.NET Core, где подчеркивается важность правильной обработки потоков для бинарных данных.

Контроль со стороны клиента

Множественные вызовы дают клиентам больше контроля над обработкой данных. Клиенты могут выбирать, какие блобы загружать, реализовывать логику повторных попыток при неудачных загрузках и более эффективно управлять параллельными запросами. Такой уровень контроля особенно ценен в мобильных приложениях, где пропускная способность и соединение ограничены. Согласно Mobile Web Best Practices, granular‑запросы улучшают пользовательский опыт, позволяя прогрессивно загружать контент.

Масштабируемость сервера

Когда серверы используют отдельные эндпоинты для бинарных данных, они могут масштабировать обработку метаданных и бинарных ресурсов независимо. Такая архитектура позволяет использовать специализированные серверы для разных типов контента (например, выделенные файловые серверы для бинарных данных) и обеспечивает более эффективное распределение нагрузки по инфраструктуре. Исследования из Amazon Web Services показывают, что разделение ответственности может улучшить общую масштабируемость и надёжность системы.


Паттерны реализации и лучшие практики

Дизайн URL‑ресурсов

Распространённый паттерн использует иерархические URL‑адреса для разделения обязанностей:

http
GET /api/foo/{id}                    # Возвращает только метаданные
GET /api/foo/{id}/files/{fileId}      # Возвращает конкретный блоб файла
HEAD /api/foo/{id}/files/{fileId}     # Возвращает метаданные файла (размер, тип и т.д.)

Такой подход следует принципу ориентированного на ресурсы дизайна, где каждый блоб рассматривается как подресурс основного объекта. OpenAPI Specification предоставляет отличную поддержку описания таких паттернов с чётким разделением между эндпоинтами метаданных и бинарных данных.

Условные запросы

Множественные вызовы позволяют реализовать сложные паттерны условных запросов:

javascript
// Первый запрос – получение метаданных
const metadata = await fetch('/api/foo/123');

// Проверяем, был ли файл изменён
const etag = metadata.headers.get('ETag');
const lastModified = metadata.headers.get('Last-Modified');

// Условный запрос файла
const fileResponse = await fetch('/api/foo/123/files/456', {
  headers: {
    'If-None-Match': etag,
    'If-Modified-Since': lastModified
  }
});

Этот паттерн использует механизмы кэширования HTTP, чтобы избежать ненужной передачи данных, как описано в HTTP Caching Best Practices.

Прогрессивная загрузка

Для приложений с сложными структурами данных множественные вызовы позволяют реализовать стратегии прогрессивной загрузки:

javascript
// 1. Получаем базовые метаданные сначала
const basicMeta = await fetch('/api/foo/123?fields=id,name');

// 2. Загружаем превью
const previews = await Promise.all(
  basicMeta.previewIds.map(id => fetch(`/api/foo/123/previews/${id}`))
);

// 3. Загружаем полные файлы по запросу
const fullFiles = await Promise.all(
  basicMeta.fileIds.map(id => fetch(`/api/foo/123/files/${id}`))
);

Такой подход обеспечивает мгновенную ценность для пользователей, одновременно загружая дополнительный контент постепенно, улучшая восприятие производительности.


Когда ответы multipart/form-data оправданы

Ограниченные пропускные способности

В сценариях с крайне ограниченной пропускной способностью (например, спутниковые соединения, мобильные сети с высокой задержкой) один multipart‑ответ может быть предпочтительнее, чтобы минимизировать накладные расходы на несколько HTTP‑соединений. Как отмечено в Web Performance Optimization, multiplexing HTTP/2 снижает накладные расходы на несколько запросов, делая multipart‑ответы менее выгодными.

Атомарные транзакции

Когда клиент требует, чтобы все данные обрабатывались атомарно (либо все части успешно, либо ни одна), multipart/form-data обеспечивает встроенное транзакционное поведение. Это особенно важно для операций, где частичные данные могут вызвать ошибки приложения. Спецификация RFC 7578 описывает транзакционную природу multipart/form-data для отправки форм.

Интеграция с наследуемыми системами

Некоторые наследуемые системы или сторонние API могут требовать multipart‑ответы для совместимости. В таких случаях поддержание согласованности с существующими паттернами API может превалировать над архитектурными преимуществами отдельных эндпоинтов. Согласно API Design Patterns, согласованность с существующими системами часто важнее теоретической чистоты.


Альтернативные подходы для сложной передачи данных

Кодирование метаданных в бинарном формате

Некоторые серверы кодируют блобы в base64 внутри JSON‑ответов:

json
{
  "metadata_field1": "value1",
  "metadata_field2": "value2",
  "file": {
    "filename": "document.pdf",
    "content_type": "application/pdf",
    "data": "JVBERi0xLjQKJcOkw7zDtsO...base64encodeddata..."
  }
}

Хотя такой подход упрощает дизайн API, тесты производительности показывают, что base64‑кодирование увеличивает размер данных примерно на 33 % по сравнению с бинарной передачей, что делает его менее эффективным для больших файлов.

Подписанные URL

Для безопасной доставки файлов сервер может генерировать подписанные URL, которые позволяют прямой доступ к хранилищу блобов:

javascript
// Ответ сервера
{
  "metadata": { /* … */ },
  "file_urls": {
    "document.pdf": "https://storage.example.com/file.pdf?signature=…",
    "image.jpg": "https://storage.example.com/image.jpg?signature=…"
  }
}

// Клиент загружает файлы напрямую
const fileResponse = await fetch(fileUrls['document.pdf']);

Этот паттерн снижает нагрузку на сервер и использует возможности CDN для оптимальной производительности, как описано в документации AWS Signed URL.

Потоковое передача через WebSocket

Для реального времени приложения WebSocket‑соединения могут использоваться для потоковой передачи как метаданных, так и бинарных данных:

javascript
// WebSocket‑соединение
const ws = new WebSocket('wss://api.example.com/foo/123/stream');

ws.onmessage = (event) => {
  if (event.data instanceof Blob) {
    // Обработка бинарного блоба
    processFile(event.data);
  } else {
    // Обработка обновлений метаданных
    const metadata = JSON.parse(event.data);
    updateUI(metadata);
  }
};

Такой подход обеспечивает лучшую производительность для передачи данных в реальном времени и особенно полезен для приложений, требующих живых обновлений сложных структур данных.


Заключение

Предпочтение множественных вызовов API над multipart/form-data ответами в REST‑API обусловлено несколькими ключевыми архитектурными преимуществами:

  1. Лучшее использование кэша – granular‑кэширование метаданных и бинарных ресурсов снижает потребление полосы пропускания и повышает производительность.
  2. Поддержка частичного контента – запросы диапазона позволяют эффективно загружать большие файлы с выборочным получением.
  3. Ориентация на ресурсы – трактовать каждый блоб как отдельный ресурс соответствует принципам REST и улучшает масштабируемость системы.
  4. Контроль со стороны клиента – множественные вызовы дают клиентам гибкость в обработке данных, повторных попытках и управлении параллельными запросами.
  5. Эффективность памяти – потоковая передача предотвращает перегрузку памяти при работе с большими бинарными файлами.

Хотя ответы multipart/form-data могут быть оправданы в специфических сценариях (ограниченные пропускные способности, атомарные транзакции, наследуемые системы), архитектурные выгоды отдельных эндпоинтов обычно перевешивают простоту одного multipart‑ответа. Для современных веб‑приложений паттерн множественных вызовов обеспечивает лучшую производительность, масштабируемость и поддерживаемость при работе со сложными объектами, содержащими блобы.

При проектировании API для сложных структур данных учитывайте конкретный сценарий и выбирайте подход, который лучше всего балансирует производительность, простоту и архитектурную согласованность. Для большинства приложений паттерн множественных вызовов будет оптимальным выбором.

Источники

  1. Mozilla Developer Network – HTTP multipart
  2. Google Web Fundamentals – Performance optimization
  3. ASP.NET Core – Reading multipart form data
  4. OpenAPI Specification – File uploads
  5. Stanford Web Performance Lab – Caching research
  6. HTTP Caching Best Practices
  7. Mobile Web Best Practices
  8. Web Performance Optimization with HTTP/2
  9. RFC 7578 – multipart/form-data specification
  10. AWS S3 Signed URLs documentation
Авторы
Проверено модерацией
Модерация