RFC 9457 problem+json стандартизирует ответы об ошибках как JSON-объект с type, title, status, detail и instance, отдаваемый как application/problem+json, — чтобы клиенты разбирали ошибки единообразно. Пагинация разбивает большие коллекции на страницы, используя cursor (стабильный) или offset (простой), со ссылками next/prev, переносимыми в заголовке Link по RFC 8288.
problem+json: стандартный объект ошибки
Самодельные формы ошибок вынуждают каждого клиента отдельно обрабатывать каждый API. RFC 9457 (который делает устаревшим RFC 7807) фиксирует единый расширяемый JSON-формат ошибок, отдаваемый с медиатипом application/problem+json, чтобы клиент знал разбирать его как problem. У объекта пять стандартных членов: type — URI, идентифицирующий класс проблемы и одновременно служащий ссылкой на человекочитаемую документацию; title — короткое, стабильное, человекочитаемое резюме этого класса; status — код состояния HTTP, продублированный в теле; detail — человекочитаемое объяснение, специфичное для этого случая; и instance — URI, идентифицирующий именно этот случай. Любые дополнительные члены (массив ошибок валидации, request id, подсказка повтора) расширяют объект, не ломая стандартные, поэтому обобщённый клиент читает общие поля, а специфический — ещё и расширения.
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-пагинация — правильное значение по умолчанию.
| Свойство | Смещение | Курсор (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 уже понимает.
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, пока он не исчезнет.
Технические характеристики источника
На этой странице представлены опубликованные стандарты, на которых они основаны. (Открывается в новой вкладке)