# Ошибки API (problem+json) и пагинация (cursor, заголовок Link)

> RFC 9457 problem+json стандартизирует ответы об ошибках как JSON-объект с type, title, status, detail и instance, отдаваемый как application/problem+json, — чтобы клиенты разбирали ошибки единообразно. Пагинация разбивает большие коллекции на страницы, используя cursor (стабильный) или offset (простой), со ссылками next/prev, переносимыми в заголовке Link по RFC 8288.

- Канонический URL: https://www.broadnet.me/ru/reference/api-errors-and-pagination
- Язык: ru
- Последнее обновление: 2026-07-19
- Издатель: BroadNet (https://www.broadnet.me)

Два стандарта, удерживающих HTTP API предсказуемым: RFC 9457 problem+json для машиночитаемых ошибок (type, title, status, detail, instance) с таксономией retryable/terminal и пагинация — cursor против offset, заголовок Link по RFC 8288 для next/prev и ограничения размера страницы.

## problem+json: стандартный объект ошибки

Самодельные формы ошибок вынуждают каждого клиента отдельно обрабатывать каждый API. RFC 9457 (который делает устаревшим RFC 7807) фиксирует единый расширяемый JSON-формат ошибок, отдаваемый с медиатипом application/problem+json, чтобы клиент знал разбирать его как problem. У объекта пять стандартных членов: type — URI, идентифицирующий класс проблемы и одновременно служащий ссылкой на человекочитаемую документацию; title — короткое, стабильное, человекочитаемое резюме этого класса; status — код состояния HTTP, продублированный в теле; detail — человекочитаемое объяснение, специфичное для этого случая; и instance — URI, идентифицирующий именно этот случай. Любые дополнительные члены (массив ошибок валидации, request id, подсказка повтора) расширяют объект, не ломая стандартные, поэтому обобщённый клиент читает общие поля, а специфический — ещё и расширения.

*Тело ошибки RFC 9457 problem+json*

```json
HTTP/1.1 422 Unprocessable Content
Content-Type: application/problem+json

{
  "type": "https://example/problems/invalid-destination",
  "title": "Invalid destination number",
  "status": 422,
  "detail": "The field 'to' is not a valid E.164 number.",
  "instance": "/v1/messages/req-01HZY9",
  "errors": [ { "field": "to", "issue": "not_e164" } ]
}
```

## Type, title и instance

Дисциплина, делающая problem+json полезным, — держать type и title стабильными и машинно-ориентированными, позволяя detail и instance меняться от случая к случаю. Клиент должен ветвиться по type (стабильному URI), а никогда по прозе в detail или title, поскольку detail волен меняться и может быть локализован. URI type в идеале ведёт к документации по этому классу ошибок, чтобы разработчик, перейдя по нему, попал на объяснение и способ устранения. instance идентифицирует конкретный отказавший запрос — полезно процитировать в тикете поддержки — и часто зеркалит request id, который API также возвращает в заголовке. Поскольку type и title идентифицируют класс, а status/detail/instance описывают случай, один тип проблемы можно переиспользовать для многих отказов без двусмысленности.

## Таксономия ошибок: retryable против terminal

Помимо формы клиенту нужно знать, что делать, и на это отвечает класс кода состояния. 4xx — это ошибка клиента: запрос в отправленном виде никогда не будет успешным, поэтому повторять его без изменений бессмысленно — исправление в том, чтобы поправить запрос (400/422 для неверного ввода, 401/403 для аутентификации, 404 для отсутствующего ресурса, 409 для конфликта). 5xx — это ошибка сервера или вышестоящей системы: запрос вполне может стать успешным позже, поэтому его можно повторить с выдержкой — в идеале под тем же Idempotency-Key, чтобы повтор не мог отправить дважды. 429 — особый временный случай: повторяемый, но только после задержки Retry-After. Кодирование этого в коде состояния (и подкрепление типом проблемы) позволяет клиенту автоматически выбирать между исправить-и-переотправить, выдержать-и-повторить и сдаться.

*Класс ошибки и правильное действие клиента*

| Класс | Значение | Повторная попытка возможна? | Действие клиента |
|---|---|---|---|
| 400 / 422 | Некорректный/недопустимый ввод | No | Исправьте запрос, затем отправьте его повторно |
| 401 / 403 | Не аутентифицирован / запрещено | No | Исправьте учетные данные или область действия |
| 404 | Ресурс не найден | No | Исправьте идентификатор |
| 409 | Конфликт состояния (например, повторное использование ключа) | No | Разрешить конфликт |
| 429 | Превышен лимит запросов | Да (с задержкой) | Подождите Retry-After, затем повторите попытку |
| 500 / 502 / 503 | Сбой сервера / вышестоящего узла | Да | Backoff + повторная попытка с тем же ключом идемпотентности |

## Пагинация: cursor против offset

Эндпоинт коллекции, который мог бы вернуть миллионы сообщений, обязан разбивать на страницы. Две основные модели меняют простоту на корректность. Offset-пагинация (?limit=50&offset=100) тривиальна и позволяет клиенту перепрыгнуть на произвольную страницу, но нестабильна на меняющейся коллекции: если строки вставляются или удаляются между страницами, элементы смещаются, и клиент пропускает или повторяет записи — а глубокие offset становятся медленными, поскольку хранилище должно просканировать и отбросить всё, что перед ними. Cursor-пагинация (keyset) выдаёт клиенту непрозрачный cursor, кодирующий позицию последнего увиденного элемента (например, id и метку времени); следующий запрос запрашивает строки после этого cursor. Она стабильна при конкурентных записях и остаётся быстрой на любой глубине, поскольку запрос ищет прямо к cursor, ценой отсутствия произвольных прыжков по страницам. Для высокообъёмных, преимущественно дополняемых ресурсов вроде журналов сообщений cursor-пагинация — правильное значение по умолчанию.

*Cursor против offset пагинации*

| Свойство | Смещение | Курсор (keyset) |
|---|---|---|
| Запрос | ?limit=50&offset=100 | ?limit=50&after=<cursor> |
| Переход на произвольную страницу | Да | Нет (только последовательно) |
| Стабильная работа при вставках/удалениях | Нет — элементы смещаются | Да |
| Производительность глубоких страниц | Деградирует (сканирование + отбрасывание) | Постоянная (переход к курсору) |
| Лучший вариант для | Небольшие, стабильные коллекции | Большие журналы с преобладанием добавления записей |

## Заголовок Link (RFC 8288)

Вместо того чтобы закапывать URL next/previous в тело, API может нести их в стандартном заголовке Link, определённом RFC 8288 (Web Linking), используя зарегистрированные типы отношений — next, prev, first, last. Клиент переходит по ссылке rel="next", чтобы продвинуться, не конструируя URL сам и не зная схему cursor, что удерживает механизм пагинации полностью под контролем сервера и позволяет ему развиваться (offset сегодня, cursor завтра), не ломая клиентов. Распространённый подход «с поясом и подтяжками» также дублирует ссылки в тело JSON для клиентов, не читающих заголовки, но заголовок Link — стандартизированный, транспортного уровня дом для них и тот же механизм, который браузер или дисциплинированный клиент API уже понимает.

*Пагинация через заголовок Link по RFC 8288*

```http
HTTP/1.1 200 OK
Content-Type: application/json
Link: </v1/messages?after=01HZY8Q3M6&limit=50>; rel="next",
      </v1/messages?limit=50>; rel="first"

{ "data": [ /* up to 50 messages */ ],
  "page": { "count": 50, "has_more": true } }
```

## Ограничения и значения по умолчанию размера страницы

Размер страницы должен быть ограничен на сервере, а не оставлен клиенту, иначе один запрос огромной страницы становится вектором отказа в обслуживании и источником непредсказуемой задержки. Паттерн — разумное значение по умолчанию (используемое, когда клиент опускает limit) и жёсткий максимум, к которому сервер молча приводит, — клиент, запросивший больше, просто получает максимум. Объявление и действующего размера страницы, и того, остались ли ещё страницы (флаг has_more или наличие ссылки rel="next"), позволяет клиенту безопасно зациклиться: продолжать переходить по next, пока next не исчезнет. В сочетании с cursor-пагинацией и ошибками problem+json это даёт API коллекций, предсказуемый в потреблении на любом масштабе. BroadNet документирует свои конкретные коды ошибок и пагинацию для разработчиков.

## Частые вопросы

### Что такое problem+json и зачем его использовать?

problem+json (RFC 9457, который делает устаревшим RFC 7807) — это стандартный JSON-формат ошибок, отдаваемый как application/problem+json, с членами type, title, status, detail и instance. Он позволяет каждому клиенту разбирать ошибки одинаково вместо отдельной обработки самодельной формы под каждый API, и он расширяем, поэтому дополнительные члены можно добавлять, не ломая стандартные.

### По какому полю problem+json клиенту следует ветвиться?

По type — стабильному URI, идентифицирующему класс ошибки. title и detail человекочитаемы и могут меняться или быть локализованы, поэтому код никогда не должен переключаться по их прозе. URI type в идеале ведёт к документации по этой ошибке, тогда как status, detail и instance описывают конкретный случай.

### Как клиент узнаёт, повторять ли ошибку?

По классу статуса. 4xx — это ошибка клиента, которая без изменений не будет успешной, поэтому исправьте запрос, а не повторяйте. 5xx — это отказ сервера/вышестоящей системы, который может стать успешным позже, поэтому повторите с выдержкой под тем же ключом идемпотентности. 429 повторяем, но только после задержки Retry-After.

### В чём разница между cursor- и offset-пагинацией?

Offset (?limit&offset) прост и поддерживает прыжок на любую страницу, но нестабилен, когда строки вставляются или удаляются между запросами, и замедляется на глубоких offset. Cursor-пагинация (keyset) передаёт непрозрачный маркер последнего увиденного элемента и запрашивает строки после него — стабильна при конкурентных записях и быстра на любой глубине, но только последовательна. Cursor — правильное значение по умолчанию для больших журналов сообщений.

### Чем заголовок Link по RFC 8288 помогает пагинации?

Он несёт URL страниц next, prev, first и last в стандартном заголовке, используя зарегистрированные типы отношений, поэтому клиент переходит по rel="next", чтобы продвинуться, не строя URL и не зная схему cursor. Это удерживает механизм пагинации под контролем сервера и свободным для развития, и это механизм, который обобщённые HTTP-клиенты уже понимают.

### Зачем ограничивать размер страницы на сервере?

Потому что неограниченный размер страницы позволяет одному запросу вытянуть громадный результат, что является риском отказа в обслуживании и даёт непредсказуемую задержку. Сервер применяет значение по умолчанию, когда limit опущен, и молча приводит к жёсткому максимуму, а также объявляет, остались ли ещё страницы, чтобы клиенты могли зациклиться, переходя по next, пока он не исчезнет.

## Источники

- [IETF RFC 9457 — Problem Details for HTTP APIs](https://datatracker.ietf.org/doc/html/rfc9457)
- [IETF RFC 8288 — Web Linking (Link header)](https://datatracker.ietf.org/doc/html/rfc8288)

## См. также

- [Проектирование REST API сообщений](https://www.broadnet.me/ru/reference/rest-messaging-api-design)
- [Идемпотентность и ограничение скорости](https://www.broadnet.me/ru/reference/idempotency-and-rate-limiting)
- [Аутентификация API и подпись запросов](https://www.broadnet.me/ru/reference/api-authentication-and-signing)
- [Коды ошибок BroadNet](https://www.broadnet.me/ru/developers/error-codes)
- [Руководство по пагинации BroadNet](https://www.broadnet.me/developers/pagination)
