Как стать автором
Обновить

Комментарии 110

Обязательно (и об этом джуниоры не знают) в ответ возвращайте ошибки. Это семантично и правильно.

А ФБ, яндекс, вроде инстаграм и нетфликс об этом не знают и возвращают 200 на всё, наверно глупые.
Эта тема многократно обсуждалась на хабре. Если бы вы писали, например, мобильные приложения на типизированных языках, то не были бы столь категоричны. Я в своих сервисах тоже везде отвечаю 200 со стандартным телом, где есть поле isSuccess, есть код и есть описание ошибки, в случае ее наличия. Это удобнее для всего, начиная от логирования и заканчивая клиентом.
Согласен с автором статьи.
Если бы вы писали, например, мобильные приложения на типизированных языках, то не были бы столь категоричны. Я в своих сервисах тоже везде отвечаю 200 со стандартным телом, где есть поле isSuccess, есть код и есть описание ошибки, в случае ее наличия.

Пишу на swift, до этого на obj-c писал. И нет, коды ошибок http крайне важный и помогают на этапе до парсинга определить что произошла ошибка. И я всегда настаиваю при работе с бекенд-коллегами чтобы ошибки отдавались с соответствующим http кодом.
Например что-то произошло с авторизацией. Слетела она по какой-то причине. Можем послать 401 код, в любом запросе, и это будет означать что авторизация не прошла — надо локально почистить данные и выполнить специфичные для этой ошибки действия (например попросить авторизоваться).
А когда всё приходит со статусом 200 — то нужно каждый раз разбираться и парсить — ошибка тут пришла (и какая — надо просто показать сообщение или нужны действия) или объект.

P.S. вообще, то что описано очень похоже на то, как реализован REST модуль в Yii2. Одна из самых удобных, на мой взгляд, реализаций, REST, с которыми мне приходилось работать.
Отображать всё множество ошибок приложения на нищебродский список HTTP кодов, а потом обратно, поддерживать этот маппинг, всем действующим лицам запоминать, обновлять спеки, делать два сложных уровня обработки ошибок вместо одного — так себе занятие, для не сильно занятых перфекционистов, прямо скажем.

Потому здоровый минимум ответов бэка, например:
200 OK — сервер справился с запросом, что из этого вышло — читайте в приложенном объекте,
403 Forbidden — низзя,
404 Not Found — не туда,
500 Internal Server Error — случилось что-то очень страшное, нет смысла объяснять что именно, действуйте по ситуации.

Ну и 401 Unauthorized, если хочется на прикрытые эндпоинты достукиваться вот прям из браузера.

Остальное — в поле .error, по отсутствию которого легко определить удавшийся на 100% запрос.

Затариваться на проекте всей номенклатурой HTTP кодов, а также PUT, PATCH, DELETE и прочим сатанизмом — прямой путь к знакомству с особенностями работы корпоративных и просто кривых проксей. Упомянутые монстры соцсетизма не просто так эти дела упростили, а чтобы сэкономить на поддержке клиентов у которых «тормозит, не работает, ставлю 1 балл пока».

Другое личное ИМХО в том, что разрешать в продакшне неконтролируемые запросы от фронта (свободные фильтры с неограниченными параметрами и прочую логику построения запроса к модели, в REST форме или любой другой — дичайший моветон и грабли.

Берегите лбы, товарищи!
Согласен. Совсем упарываться не надо, и описанное в статье неплохо в теории и в идеале, но всегда «реальность вводит коррективы».
Собственно я такой блок ответов и использую.
200 — ок
401/403 — проблема авторизации (+ error поле)
400/500 — просто ошибка, что-то пошло не так (+ error поле)
Я думаю, коды ошибок HTTP следует оставить для ситуаций, когда отвечает не знакомый REST-сервер, а оказавшееся на его месте нечто другое: прокси, свежеустановленный пустой веб-сервер, чужой сайт или REST-сервер, заглушка роскомцензуры. И, соответственно, при обработки этих кодов следует исходить из того, что никакого json'а в теле ответа нет, и, соответственно, не пытаться его искать и парсить.
А серверу, в свою очередь, следует использовать эти коды, если есть серьёзные основания полагать, что к нему пришёл вообще не REST-клиент, а нечто странное: опечатавшийся человек, гугловый робот и т.п.
Я бы к этому списку ещё 400 Bad Request добавил
Проще:
2хх — всё удачно
4хх — Вы чет не то прислали
5хх — Ой, чет на сервере набаранили

3хх — сервисные
Как вы отличаете нет соли или нет магазина с солью? 404 потому что uri битый или 404 потому что /items/1 не существует?
И даже страшно представить сколько у вас бойлерплейта на клиенте для сортировки ошибок.
Я так не загоняюсь. Нет соли в магазине — значит например 400/404/любой другой с сообщением (полем error).
404 битый url — просто мы не получим сообщение от json в body от сервера и выводим ошибку вида «что-то пошло не так, попробуйте позже или сообщить нам о проблеме»
В обоих случаях вам приходит одинаковая 404. Как вы решаете, что показать юзеру? А если в случае «нет соли в магазине» вам приходит пояснение в теле, то как вы определяете надо ли парсить тело?
Да, тело парсится всегда. Если не найдено ничего в теле — то показывается «неизвестная ошибка». А так — всегда в теле должно быть что-то
Да, всегда одинаковая. Если нет тела, то показываем «универсальную» ошибку.
Я больше придерживаюсь принципа — если всё ОК — то код 200.
Во всех остальных случаях — любой не 2хх код и текст ошибки в теле.
А 401 и прочее — это для обработки «особых» случаев и особых ошибок.
Ну можно упороться и в тело помимо ошибки определить ещё какой-то протокол, по которому определять действия. Но это уже изврат я считаю.
Я правильно понимаю, что у вас сделано так: если 200, то парсим тело в полезный объект. Если 404, то смотрим в тело, если оно есть, то парсим в объект ошибки, если нет, то показываем ошибку?
Да

А с моим подходом при любом 200 тело парсится в один и тот же объект. А все остальные ответы сразу отправляются в невалидные, потому что они возможны только при отладке или падении чего-то. Это значительно упрощает клиента.

Тоже хорошой подход. Осталось заставить бекенд разработчиков поддерживать единообразие. К сожалению, в моей работе вполне возможны варианты, когда формат ответа меняется, выкатывается на бой, а мобильных разработчиков предупредить забыли.
+ бывают разные типы ошибок.
Поэтому я предпочитаю, что если успех — то это success блок, и отрабатывается по одному алгоритму.
А если ошибка — то в отдельном месте определяем что за ошибка (от сервера пришла, сетевая ошибка, сервак лёг) и обрабатываю это отдельно.
Мне кажется что чёткое разделение — вот здесь точно нет ошибки,
а здесь точно ошибка — более гибкое и меньше всяких условий получается.
Но это моё ИМХО, основанное именно на моём опыте.

Многие либы обертки для http бросают исключения при не 200, а это противоестественно когда код ветвится за счет try catch, для этого придуман if.

То-то Netflix либу для JSON API поддерживают, github.com/Netflix/fast_jsonapi, чтобы потом двухсоточки возвращать. Впрочем не удивлюсь, если какие-то отделы это делают, возможно в силу каких-то объективных причин, типа телевизоров или приставок, не умеющих в http коды или чтобы вообще не парсить http заголовок, хотя сильно сомневаюсь.

Задам встречный вопрос, может фб, Инстаграм и Яндекс просто знают, что многие, кто будет использовать их апи, не читают стандарты и не поймут эти коды или проигнорируют их? Или что пользователи напишут что-то вроде
raise if response.code >= 400
к примеру. И давать советы забить на http коды в общем-то только поощряют эту практику.

Никто в принципе не заставляет пользователей апи использовать http коды, если удобно проверять поле error какое-нибудь. Но возвращать их — хороший подход. И писать клиент, используя их, в общем случае проще и удобнее.

от логирования
если только логируете вы в всё в один файл и пользователей у вас 1.5 человека. Фильтровать по статусу весьма удобно, когда это необходимо.
В докладе я говорю о том, что любое решение будет работать. Но вместо того, чтобы спорить по поводу правильных HTTP-кодов и названий полей, вроде isSuccess, лучше взять готовую спеку и начать делать то, что нужно заказчику.
HTTP был предложен в марте 1991 года Тимом Бернерсом-Ли, а Roy T. Fielding присоединился в 1993… Один из основателей Apache HTTP Server Project.
Спасибо за замечание. Согласен, слово «придумал» не очень корректно. Я имел ввиду, что он является одним из авторов.
Поясните пожалуйста, зачем указывать тип объекта еще раз в запросе
GET /articles?fields[article]=title НТТР/1.1

Через include можно наподключать смежных сущностей, а через fields задать полей, которых мы будем ждать в смежных сущностях.
Например:
GET /articles?include=authors&fields[article]=title&fields[author]=id,name

Ну, все равно получается избыточность. Если для include, то понятно. Но если не указывать тип, можно предположить что поля для того же объекта, что и запрос. Т.е. мне лично такой синтаксис кажется избыточным. Я бы сделал что-то типа:
GET /articles?fields=title&include=authors&fields[author]=id,name
Получается немного, да.
Но я думаю, что при составлении спецификации решили, что в данном случае единообразие интерфейса и минимизация ошибок важнее избыточности.

В вашем примере fields одноременно строка и массив. Сервер просто проигнорирует fields=title в подобном запросе.

Это зависит от реализации сервера.
RFC 3986 никак не определяет формат передачи массива через GET-параметры.


В общем случае на сервер придёт строка вся query string.


Если сервер разберёт её по аргументам, то, скорее всего, будет вот так:


{
  'fields': 'title',
  'include': 'authors',
  'fields[author]': 'id,name'
}

Если игнорирование и будет, то только если оно специально так настроено.

Вы правы, мое замечание относится только к PHP. Он распарсит эту строку как я написал:


array(2) {
  ["include"]=>
  string(7) "authors"
  ["fields"]=>
  array(2) {
    ["article"]=>
    string(5) "title"
    ["author"]=>
    string(7) "id,name"
  }
}

http://sandbox.onlinephpfunctions.com/code/549e94093e8565c828811eb2506f80d09a2fa390

Причём не просто к PHP а к его стандартным способам парсинга. Вы в полном праве игнорировать их наличие и парсить query string и(или) тело запроса как угодно. В частности никто не мешает вам отправлять в query string json.

Ну, наверное, вот для этого:


GET /articles?include=author&fields[article]=title,body&fields[person]=name

Чтобы стандартный парсер Query String в любом веб-фреймворке преобразовал это в


{
  include: "author",
  fields: {
    article: "title,body",
    person: "name"
  }
}
Статья вроде рассчитана на начинающих бекендеров, и сразу предлагается в Rest запихивать кучу условий для SQL запросов. Что-то мне подсказывает, что начинающий бекендер наплодит такой «гибостью» кучу дыр для SQL-инъекций…
Даже в случае использования параметрических запросов? ИМХО конечно, но если %framework% на проекте не совсем древний / дремучий, то на сегодняшний день не использовать параметризацию (кроме очень особых случаев) — это просто какая-то дичь. Code review, опять же, на то и тимлид / сеньоры, чтобы джун не дремал.

Тяжело использовать параметрические запросы с переменным числом параметров…

Не совсем понял, почему тяжело?
Потому что нужно согласованно собирать сам запрос и список параметров в двух разных переменных, да ещё и назначать всем уникальные идентификаторы — а это почти невозможная задача для FullStackOverflow-девелоперов :-)
Не вполне согласен. Во-первых, при использовании современных фреймворков решение вышеозначенной задачи зашито в подсистеме абстракции БД, и работа того джуна оказывается на уровне элементарной профпригодности («посмотри как сделано десятью строками выше, и сделай похожим образом вот тут»). Во-вторых, может быть у нас разные рынки труда, но у меня на работе джуны способны и читать и писать простейшие манипуляции с массивами. Code review и автотесты ИМХО вполне способны закрыть большинство оставшихся проблем.
Я тоже так думал, пока не увидел как в унаследованном проекте собирались запросы.
Не предлагается сразу всё запихивать. Большинство условий являются необязательными и просто иллюстрируют «как можно делать, если Вам нужно».
Ну а вообще, приоритет универсальности спецификации API далеко не всегда оправдан.
На самом деле, автор исходит из посылки, что изменение API — это сильно больнее сложности.
На практике, я часто вижу примеры, когда перебарщивают с универсальностью, порождая сложность. А потом оказывается, что эта универсальность не так уж и нужна. Зато сколько времени потрачено на вылизывание всяких гипотетических кейсов от тестеров, когда шлются дикие комбинации параметров, которые ломают логику, и тестеры радостно кричат «Ага!». Когда запрос четко валидируется на на уровне схемы и мапится в статику, это делает API сильно проще и надежнее. А добавить поле в ответ — ну, отрефакторим. Если нормально налажена работа в команде и бэкенд не пребывает в состоянии холодной войны с фронтендом, то никакой драмы. Ну и версионирование никто не отменял.
Согласен, валидация (всякими JSON Schema) необходима — если запрос не по схеме, то возвращать что-то типа 400 с описанием этой самой ошибки (что необходимо, что лишнее). Потому что если запрос не проходит валидацию, то даже пытаться его обработать смысла особого нет. Соответственно, и мапинг в статику — тоже очень полезная вещь (конечно, когда эта статика есть), это обнаружит потенциальные проблемы на уровне компиляции (опять же, когда она есть), а не после деплоя (или, еще хуже, на продакшене).
Ну к слову про связи, можно развесистые графы свойств именовать и на бекенде регистрировать, а клиенты должны будут передать только имя представления.

1. Ограничивает жадность клиентов.
2. Позволяет извлекать большие сложные графы, описание которых сложно затолкать в URL.
Странно что ничего не упомянули про JSON-RPC v2.0
Вполне себе хорошая вещь.
Если про RPC говорить, то, наверное, и про gRPC надо вспомнить. Но тут же вроде про REST.
Про RPC было в ранней версии доклада, но пришлось убрать, чтобы уложиться в полчаса и донести мысль не очень скомкано. :)

Остались только ссылки в конце презентации на спецификации XML-RPC и JSON-RPC для дальнейшего изучения.
Увидел метод PATCH и вздрогнул — похоже автор забывает о такой сфере утилизации человеческих ресурсов, как «системное администрирование». Скучающий сисадмин запросто может настроить корпоративный прокси на поддержку GET, POST запросов и блокирование остальных видов запросов, как «опасных», что приведет к поломке нашего API, использующего PATCH.

И еще возник вопрос допустимости использования не закодированных символов "[" и "]" в URL.
В спеке есть про это, если я правильно понял Ваше замечание.
Some clients, like IE8, lack support for HTTP’s PATCH method. API servers that wish to support these clients are recommended to treat POST requests as PATCH requests if the client includes the X-HTTP-Method-Override: PATCH header. This allows clients that lack PATCH support to have their update requests honored, simply by adding the header.
jsonapi.org/recommendations/#patchless-clients

На вопрос тоже нашёл ответ в спеке:
Note: The above example URI shows unencoded [ and ] characters simply for readability. In practice, these characters must be percent-encoded, per the requirements in RFC 3986.
jsonapi.org/format/#fetching-sparse-fieldsets
Some clients, like IE8...

— нет, не правильно, дело не в клиенте, а в настройках корпоративного proxy-сервера которые режут все запросы клиента, кроме простейших GET/POST. Это касается любых клиентов, Хром, FF-наипоследнийший — не важно. К серверу запрос придет от прокси или не придет. Короче, экзотические типы запросов, это риск потерять часть функциональности для некоторых пользователей. Ваше замечание со ссылкой на спецификацию, тоже актуально, так как редкий сервер или бэкенд фреймворк будет конвертировать «X-HTTP-Method-Override: PATCH» в PATCH (точно не знаю, не проверял, но испытываю сильнейшие сомнения). Как результат на бэкенде мы получим POST, если запрос вообще до нас дойдет.

these characters must be percent-encoded

— это я и имел в виду, из статьи это явно не следует и велика вероятность что фронтэнд разработчик забудет закодировать такие символы
Как организовать удаление нескольких объектов одним DELETE-запросом, используя «правильный» REST API? DELETE-запрос не должен содержать тела.

http://springbot.github.io/json-api/extensions/bulk/#deleting-multiple-resources


DELETE /articles
Content-Type: application/vnd.api+json; ext=bulk
Accept: application/vnd.api+json; ext=bulk

{
  "data": [
    { "type": "articles", "id": "1" },
    { "type": "articles", "id": "2" }
  ]
}

Note: RFC 7231 specifies that a DELETE request may include a body, but that a server may reject the request. This spec defines the semantics of a server, and we are defining its semantics for JSON:API.
> Когда на сервере хранится сессия, и в зависимости от этой сессии приходят разные ответы, это нарушение принципа REST.

Слегка не корректный тезис. И в целом это не так. Хотя подозреваю, что может зависеть от реализации.

Например, разве это не разные запросы, которые должны давить разные ответы:

/users
/users/1
/users/2

???
Предлагаю минусующим раскрыть свою точку зрения в ответом комментарии. Мне кажется это будет полезнее для общей эрудиции.

А как отличие между /users/1 и /users/2 вообще связано с сессией пользователя?

Очень даже связанно) Пример показывает очевидный способ реализации постулата «запрос содержит все что нужно серверу для ответа». Именно поэтому сервер может быть stateless. Так вот, что происходит тут:

/users/1

Сервер понимает, что ему нужно отдать юзера с ID = 1, лезет в БД и достает всю необходимую информацию. Так?

Теперь посмотрим вариант с сессией:

/me + куках session_id

В этом случае происходит все ровно тоже самое. Запрос содержит в себе все что нужно серверу для ответа. Сервер понимет, что ему нужно извлечь информацию для сессии с данным ID, лезет в базу или файлы, в зависимости от того, где лежат сессии и возвращает ответ.

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

Основная мысль в том, что сессия — это такая же часть запроса, которая никак не влияет та то, является ли сервер stateless. Для простоты вынесем сессию из кук в get-параметры:

/users/1 — данные пользователя с ID = 1
/users/2 — данные пользователя с ID = 2
/users/null — ошибка, потому что в данном endpoint мы ожидаем ID юзера

/me?sid=lk235lk23lk5jl2k5 — данные юзера с сессией lk235lk23lk5jl2k5
/me — ошибка, потому что в данном endpoint мы ожидаем сессию

То есть, мы видим, что это разные запросы и ответы четко зависят от того, что пришло в запросе. Сервер никак не влияет на них, не хранит никакого скрытого состояния.

То что сессия скрытая от глаз и чаще всего хранится в куках, не делает сервер stateful. Наоборот, это способ временно делегировать стейт клиенту, без его участия, чтобы сервер мог оставаться stateless, а значит RESTful. Именно это позволяет серверу не хранить этот стейт у себя до следующего запроса.

Вы упускаете из виду кеширование результатов запроса. Результаты запроса /users/1 могут быть кешированы какое-то разумное время. Результаты запроса /me + в куках session_id кешированы быть не могут...


Ну и семантика у таких запросов тоже разная. /users/1 — это данные пользователя 1, независимо от того кто их смотрит.

И что? Это особенность реализации HTTP и разные виды параметров. Да, семантика передачи информации через path, query, header & cookie разные и работают не идентично, но это все равно передача информации.

Я лишь говорю о том, что запрос с сессией это такой же запрос к stateless серверу, как и без неё. И именно то, что один и тот же запрос отличающийся лишь наличием или отсутствием сессии возвращает разные результаты с сервера и доказываться что сервер истинно stateless.
клюс сессии достаточно сильно на практике отличается от того же самого jwt, содержащего все нужные для ответа данные.
В случае с jwt мы можем где-то на api gateway, policy descision point, самом сервисе обработки запроса, да даже на клиенте, замаппить /me/ в /users/1 вообще без обращения к каким-либо ресурсам, что сильно упрощает инфраструктуру и логику приложения. При этом /users/1 может шардироваться между инстансами и роутится по логике mod(userId,10), кэшироваться, кэшироваться в самом браузере на основе etag и вообще подвергаться всяческим оптимизациям просто потому, что оптимизируют как правило правильные решения, а не костыльные поделки. И информации по работе с правильными решениями значительно больше
> Хранящаяся на сервера сессия, ниче в данном случае не отличается от любых данных там же.

В общем случае отличается. Кука, особенно сессионная — атрибут сеанса связи, атрибут конкретного агента, получаемый от сервера. Чтобы получить куку нужно сначала сделать запрос на сервер, а перенос кук ручками в общем случае рассматривается как атака.
Сори, но вы тоже говорите не о том. Я написал «сессия хранящаяся на сервере», а не кука. Если данные сессии положить в базу, то вообще разницы как таковой нет.

Кука — это средство доставки параметра запроса. Один из множества вариантов, отличающийся спецификой клиента (браузера) и тем, что она управляется с сервера.

Но это никоим образом не отменяет того, что кука приходит к запросом. Без куки, сервер не вернет сессию, то есть сам по себе не хранит состояние. То есть опять же «вся необходимая информация для ответа, приходит в запросе».

Вы смотрели последний сезон «True detective»? Там он с утра встает и слушает то, что записал с вечера для себя на диктофон, потому что он не помнит. Это хорошая аналогия, чувак stateless и в курсе этого, поэтому он записывает себе куку, чтобы оставаться stateless, но иметь возможность «вспомнить» то, что нужно при следующем запросе.

Секьюрность, особенности кук и все остальное вообще к вопросу не имеет отношения никакого. Это вообще больше специфика браузерной среды, а не REST и HTTP. В этом всем вы правы.
Нет разницы, где хранится сессия на сервере. Если чтобы обработать запрос к конкретному ресурсу, в запросе должен быть идентификатор не только ресурса (URI), но и какого-то виртуального ресурса, который хранит в себе результаты предыдущих запросов конкретно этого агента (идентификатор сессии), то о stateless речи нет.
И это ключевой момент.

В таком ключе даже пример блого-апи предложенного в статье как, якобы каноничный не соответствует определению REST данному в статье же.

Другой вопрос, что определение дано неверное…

Stateless означает, что сервер не хранит никакого состояния о сессии клиента на стороне сервера.


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


Это единственный способ масштабирования до миллионов одновременно работающих пользователей.


Полее подробно с картинками описано в диссертации Роя Филдинга.

Все правильно написал, но тут нужно уточнить что ты подразумеваешь под «сессия»? Если интертификатор сессии в куке, тогда это как раз то, что передается на сервер при каждом запросе (пусть и не явно), то есть никаким образом не делает сервер более «stateful».

Если же речь о самих данных сессии, тогда нужно уточнять дальше. Хранится на сервере где? Локально на диске — это конечно блок для масштабирования, но в БД к специальной таблице — это другой случай. В конечном итоге из этой БД может читать любой инстанс сервера, поэтому это никак не может сказаться на масштабировании (масштабирование самой БД опустим, это не предмет разговора).

Резюмируя, сама по себе сессия и даже ее хранение на севрере никак не нарушает REST, но определенные способы ее применения и хранения могут в частных случаях его нарушать.
Общепринятое понимание сессии я вижу как последовательность запросов и ответов в ходе одного сеанса работы пользователя. Данные сессии — данные, которые должны быть доступны серверу для корректной отработки последовательности именно как последовательности, когда результаты ответа на конкретный запрос зависят от предыдущих запросов и ответов.
Написали очень обще, поэтому так и не понял вы оппонируете или соглашаетесь. Давайте рассмотрим лучше техническую реализацию и чем конкретно это может нарушать REST?
Адский минусущий, который как пулемет минусует весь тред, будьте так добры, либо высказывайтесь сами, либо не мешайте высказываться другим.
Любая реализация, где при запросе с клиента на сервер нужно передать любым способом идентификатор серверной сессии для, например, авторизации. Нарушение в том, что перед произвольным запросом необходимо сделать минимум ещё один запрос для открытия сессии и получения её идентификатора.
Если ресурс не доступен без какого-то дополнительного действия (в данном случае авторизации) — это не нарушение REST и не делает сервер «stateful». Это всего лишь часть бизнес-логики.

Если не согласы, тогда расскажите как это может повлиять на масштабирование и на возможность любого другого сервера/потока/инстанса, кроме того, который авторизовал юзера, обработать любой следующий запрос клиента? А ведь это главный стопер stateful-серверов — их сложно скейлить, потому что нужно гарантировать подключение к нужному инстансу, хранящему состояние.
Нет, это именно нарушение REST и именно stateful сервер. Бизнес-логика может явно или неявно подразумевать наличие сессии, но где её хранить — это деталь технической реализации. В случае если она хранится на сервере, то это stateful сервер.
Делаю вывод что вы никогда не видели stateful сервер и не прописывали таблицы маршрутизации для таких серверов. Видимо и с проблемами балансировки на stateful серверах тоже не сталкивались.

Если так рассуждать как вы, то тот факт, что на сервере вообще есть база и в ней лежит стейт, сразу делает его stateful. Да и на вопрос мой вы так и не ответили.
Нет у вас оснований для таких выводов. Таблицы маршрутизации и проблемы балансировки к REST никакого отношения не имеют. Вернее REST подаёт как решение этих проблем stateless (в отношении сессий) сервера. Если вы используете серверные сессии, пускай и с расшаренными между серверами данными, то вы решаете эти проблемы, но не через REST-подход.

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

Они имеют отношение к stateful серверам. Раз наш умозрительный сервер не нуждается в этом, значит он stateless. Уж простите, тут либо одно, либо другое. От ответа на вопрос, вы таки ушли, потому что прекрасно понимаете, что несмотря на сессию, сервер остается stateless (REST пока за скобками).

Если вы используете серверные сессии, пускай и с расшаренными между серверами данными, то вы решаете эти проблемы, но не через REST-подход.

То есть вы признаете, что сервер остается stateless, не смотря на хранение данных сессии в БД? Кроме того, где вы это прочитали в архитектуре REST? Там написано, что для обработки любого запроса, серверу нужны лишь данные из этого запроса, то есть никакой локальный для сервера стейт не нужен, чтобы выполнить запрос клиента. Как таковые сессии, способы их хранения вообще нигде не упоминаются.

В REST-подходе данные сессии хранятся на клиенте, он отправляет их на сервер с каждым запросом

Можете предоставить цитату, где в трудах о REST написано про то, что все данные сессии должны каждый раз курсировать между клиентом и сервером? Как быть когда объем этих данных выйдет за размер куки (что бывает очень часто)?

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

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

Описанный вами вариант REST лишь на идеалогическом уровне может хоть как-то конфликтовать с тем, что пишу я. Но идеалогия не пишет сервера, а вот практически получается что разницы нет.

> Раз наш умозрительный сервер не нуждается в этом, значит он stateless.

Очень узкое понимание stateless. Особенно в контексте принципов REST. Чисто технически, да, сервер (демон, сервис) stateless если он ничего в оперативной памяти не держит между запросами. Часто к этому добавляется пишет ли он что-то на ФС, но тут уже спорно: ФС — может быть примонтированной сетевой шарой, томом и т. п.

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

Там написано «No client context shall be stored on the server between requests.». Общепринято клиентский контекст (под каким юзером залогинился, какой язык выбрал для сессии и т. п.) называть сессией.

> Можете предоставить цитату, где в трудах о REST написано про то, что все данные сессии должны каждый раз курсировать между клиентом и сервером?

Не совсем так, каждый раз не должны, только когда серверу они нужны для обработки запроса клиент должен их передать, возможно получив их ранее от сервера, но не обязательно — не имеет значения откуда он их возьмёт.

> Наш умозрительный сервер, также может обработать любой запроса независимо и ему для обработки нужен только доступ к хранилищу ресурсов (БД).

Не только к хранилищу ресурсов, но и к хранилищу клиентского контекста, в котором хранится, например, под каким юзером этот конкретный клиент последний раз залогинился.

> Описанный вами вариант REST лишь на идеалогическом уровне может хоть как-то конфликтовать с тем, что пишу я.

Нет, на вполне техническом. Если сервер определяет какую запись из БД показывать (и показівать ли) для URL /me на основании того какое имя пользователя и пароль были отправлены на /login, то это различие на вполне техническом уровне: ему нужно помнить что было отправлено и(или) какой результат обработки был.
Очень узкое понимание stateless.

Нет, это просто очень практическое понимание. Любые дополнительные смыслы, которые вы накладываете не более чем идеология. Если сервер не имеет непереносимого состояния и может скейлится безболезненно на любое кол-во инстансов, значит он stateless по определению.

Общепринято клиентский контекст (под каким юзером залогинился, какой язык выбрал для сессии и т. п.) называть сессией.

Технически настройки языка должны храниться в базе. Но даже если говорить о сессии на верных примерах, данные сессии физически не хранятся на сервере. Они храняться на сервере БД. То о чем вы пишете валидно, если брать классический PHP-шный подход с сессиями на файлах. Но я даже не уверен, что кто-то его использует сейчас. Мы же не в 2000-х ей богу.

Не совсем так, каждый раз не должны, только когда серверу они нужны для обработки запроса клиент

Вы так и не ответили на вопрос, что делать когда размер куки кончится.

Не только к хранилищу ресурсов, но и к хранилищу клиентского контекста, в котором хранится, например, под каким юзером этот конкретный клиент последний раз залогинился.

Эта информация всегда хранится в БД и также является ресурсом, например для админки проекта в разделе статистики по юзерам. Сами по себе данные сессии — ничем не отличаются от любых данных сервера. Да, они часто умеют «истекать», но такое поведение характерно далеко не только для сессий.

запись из БД показывать (и показівать ли) для URL /me на основании того какое имя пользователя и пароль были отправлены на /login,

Это не так. Сервер не пытается показать запись /me исходя из /login. Он знать не знает что клиент вообще когда-то ходил на /login, потому что это происходило в другом потоке или даже на другом физическом сервере. Текущий поток действует исходя исключительно из параметров запроса — пришел ID сессии «посмотрел» в базу, взял юзера. Не пришел ID сессии — «не посмотрел». А главное совершенно не важно откуда клиент взял этот ID. Хоть сам придумал.

это различие на вполне техническом уровне: ему нужно помнить что было отправлено и(или) какой результат обработки был.

Вообще не нужно ему это помнить. Еще раз, процесс логина 100% происходил в другом потоке, который уже почил со всем своим контекстом и на N% происходил на другом инстансе/сервере. При этом поток и сервер, который в данный момент обрабатывает /me не имеет у себя никаких рудиметнов от процесса логина и вообще знаний о существовании такого процесса.

Нет, это просто очень практическое понимание. Любые дополнительные смыслы, которые вы накладываете не более чем идеология.

Практическое понимание простое: если для ожидаемого результата запроса нужно предварительно сделать ещё один запрос (типичный пример — запрос на аутентификацию), то сервер stateful в контексте REST. Второй запрос не содержит всей необходимой информации, раз первый нужно делать.


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

Никому они не должны. Базы вообще может не быть, кроме как сессионной.


Вы так и не ответили на вопрос, что делать когда размер куки кончится.

А что делать когда место на диске с базой кончится? Кто вам сказал, что в случае хранения клиентского контекста на клиенте, он или его часть должна передаваться через куки?


Это не так. Сервер не пытается показать запись /me исходя из /login. Он знать не знает что клиент вообще когда-то ходил на /login, потому что это происходило в другом потоке или даже на другом физическом сервере.

Он чтобы это узнать лезет в сессионную базу, где в каком-то виде записано, что этот клиент ходил на /login и был идентифицирован как пользователь такой-то. Другой или тот же физический сервер, другой или тот же процесс и т. п. значения не имеет.


Вообще не нужно ему это помнить.

"сервер помнит, что юзер залогинен" в данном случае означает и "сервер знает или узнаёт, что юзер залогинен на параллельном сервер".

если для ожидаемого результата запроса нужно предварительно сделать ещё один запрос (типичный пример — запрос на аутентификацию), то сервер stateful в контексте REST.

Что значит «нужно»? Вот вам надо получить данные статьи, вы ID статьи подбирать будете научным тыком? Или вам сперва придется сделать запрос на список статей /posts, оттуда получить ID статьи и потом сделать запрос на статью /posts/:id? Исходя из вашей логики второй запрос также не возможен без предварительного первого запроса. Это тоже не REST теперь?

Никто не заставляет клиент делать запрос на аутентификацию, но если ему нужно получить /me, сама бизнес-логика требует этого просто потому, что endpoint /me требует передать ID сессии в качестве параметра. Точно также как endpoint /posts/:id требует передать ID поста.

Скажите, если запись будет такой /sessions/:id для вас это вдруг станет REST'ом?

Никому они не должны. Базы вообще может не быть, кроме как сессионной.


Что это за настройки языка, если юзер вынужден выбирать их каждый раз когда сессия закончилась? Чушь какая.

Кто вам сказал, что в случае хранения клиентского контекста на клиенте, он или его часть должна передаваться через куки?

Ок, а как еще? Будете POST запросом каждый раз в body слать? А если вам нужно будет отправить запрос на создание какой-то записи и при этом нужно будет передать сессию? Вы как это себе видите?

Он чтобы это узнать лезет в сессионную базу, где в каком-то виде записано, что этот клиент ходил на /login и был идентифицирован как пользователь такой-то. Другой или тот же физический сервер, другой или тот же процесс и т. п. значения не имеет.


Во-первых, нет никакой «сессионной базы». В одной базе может лежать таблица Users и таблица UsersSessions со связью has-many или еще как-то. Во-вторых, в таблице UsersSessions не записано что юзер куда-то там ходил. Там просто либо есть запись по session ID, либо ее нет. Либо она просрочена, либо нет.

Пишете какие-то откровенные глупости. Это тоже самое что сказать, что сервер полез в таблицу Posts и там в каком-то виде записано, что клиент ходил на endpoint создания поста. Ну бред же.

«сервер знает или узнаёт, что юзер залогинен на параллельном сервер».


WAT? Каком еще параллельном сервере? Он вообще не залогинен на сервере. Залогиненым на сервере можно быть, только если вы используете, ну я не знаю, HTTP Basic Auth что ли. Пользовательский запрос, либо имеет идентификатор сессии в системе в целом, либо не имеет. Никаких логинов «на параллельном сервере» не существует.

Если я руками на клиенте пропишу другой sessionId и передам его серверу с запросом, сервер никогда не сможет понять, что это не моя сессия. Именно потому, что он не хранит состояния напрямую связанного с клиентом, а действует исходя из параметров запроса и только.

> Вот вам надо получить данные статьи, вы ID статьи подбирать будете научным тыком?

Гугл выдаст мне ссылку и я без подбора открою. Или пришлёт кто-то в слаке. А если я должен сделать запрос на /posts перед тем как открыть /posts/:id то это не REST.

> Скажите, если запись будет такой /sessions/:id для вас это вдруг станет REST'ом?

Да, если сессия будет открываться как POST /sessions и в ответ давать 201 Location: /sessions/:id. Ну и любые изменения в сессии будут осуществляться через PUT или PATCH /sessions/:id, а другие ендпоинты не будут давать 401 при отсутствии session_id в запросе

Гугл выдаст мне ссылку и я без подбора открою.

Ого, то есть вы пишете свой фронтенд через запросы в Гугл? Необычное решение… сомневаюсь что оно REST, но определенно не стандартный подход. Ваши доводы становятся все более уклончивыми.

Если серьезно, я вас спрашивал со стороны клиента. Если вы следите за конвой диалога, то мы как бы поисковые системы вообще не упоминали. Речь идет о том, откуда вы узнаете ID поста, если не из другого endpoint?

Да и про SEO нигде в REST не написано. Что там должно индексироваться поисковиками и как.

Да, если сессия будет открываться как POST /sessions и в ответ давать 201 Location: /sessions/:id. Ну и любые изменения в сессии будут осуществляться через PUT или PATCH /sessions/:id, а другие ендпоинты не будут давать 401 при отсутствии session_id в запросе


А с чего вы взяли что это не так работает? И чем таким пренципиальным отличается POST /login от POST /sessions? Итог операции один — создание новой записи в таблица БД. Никто кстати не мешает использовать для логина POST /sessions чтобы унифицировать апи. Так сессия сразу более REST стала? Ну ладно.

а другие ендпоинты не будут давать 401 при отсутствии session_id в запросе

Совершенно не связанные вещи. Бизнес-логика может требовать сколь угодного кол-ва параметров запроса, чтобы вернуть ресурс. Для некоторых ресурсов, которым нужен доступ к сессии, нужно передать session id и нет в этом никакого нарушения REST. Это лишь параметр запроса.

Опять же, если вам будет понятнее то вот это:

/me/:sessionId или /settings/:sessionId ничем не отличается от того же самого, когда sessionId лежит в куке. Только похитить его чуточку сложнее.

Ещё одна классическая ошибка, когда рассказывают про REST — ресурс !== таблица БД.

Да, конечно.
REST — передача состояния представления.
Естественно, представление может быть получено откуда угодно.

В таком случае вообще не понятны всевозможные relationships (считай join). Потому что по идее REST не запрещает создавать любое кол-во ресурсов под каждый клиентский случай. Более того, это даже лучше с точки зрения безопастности и производительности, чем query-rich api, то есть когда клиент может составлять сложные запросы как его душе угодно.

Аппелировать к DRY тут бесполезно, потому что никакой DRY не нарушается. Создаются лишь дополнительные endpoint's, но внутренний код, которые валидирует, форматирует и достает данные не обязательно должен дублироваться каким-либо образом. Скорее всего это будет некая композиция существующего кода под каждый роут.

Опять же супер частая ситуация: проектировщик БД умный парень, заделал все с расчетом на будущее, таблиц куча, куча ключей, индексы и все по феншую. Тупое маппирование апи на таблицы станет причиной самоубийства вашего фронтендера. Но вдруг оказывается, что не смотря на то, что условный «BlogPost» состоит из самого материала, его автора, комментариев, какой-то мета-информации, информации о тематике и разделе, а также лайках и шерах, и все это лежит в разных таблицах, но на клиенте, эти данные практически не используются раздельно. Оказывается не нужны нам все эти:

/posts?query
/comments?query
/categories?query
/etc

Достаточно выдать клиенту некий ресурс «post», который уже включает в себя всю предметную область. Фронтенд доволен, бекенд доволен, потому что отвязал REST-ресурсы от таблиц БД и теперь у него развязаны руки.
Нет никакой связи с БД.
Relationships (считай join) связывает представления, которые могут быть получены откуда угодно.
Почему не сделать новый ресурс, который очевидно для бекендера свяжет нужные сущности и предоставить фронтенду ровно то, что ему нужно?
Сделай, спецификация JSON API это не запрещает. :) Она лишь говорит, что «если Вам нужны связи между представления, то вот так их можно сделать».
А ну ок.)) Из доклада мне почему-то показалось, что это как sql join своеобразный.
Чтобы не дергать бэкендера каждый раз, когда фронтендеру нужно что-то ещё или то же самое, но немного по другому, или больше не нужно. Так же чтобы не плодить ендпоинты в геометрической прогрессии по количеству связей.
Это отличная сказка. Другая крайность GQL, в котором клиент может делать с сервером практически что ему хочется. Лично я считаю, что бекенд слишком важен, чтобы опрометчиво давать столь широкий доступ для фронта. А апи они на то и нужны, чтобы работать на нужны своего юзера, поэтому дописать endpoint это норма.

Если конечно вы не делаете некий public api для неограниченного круга лиц, тогда соглашусь, надо давать больше свободы. Но мы ведь вроде не про это.
Не практически что ему хочется, а то что дозволено. И да, как раз про принципу «апи они на то и нужны, чтобы работать на нужны своего юзера» лучше написать универсальный ендпоинт, чем плодить десятки и сотни их на каждый кейсы типа «юзер захочет посмотреть статистику по продажам в разрезе регионов», «юзер захочет посмотреть статистику по продажам в разрезе городов», «юзер захочет посмотреть статистику по продажам в разрезе городов, но сгруппированных по регионам», «юзер захочет посмотреть статистику по продажам в разрезе групп товаров», «юзер захочет посмотреть статистику по продажам в разрезе регионов и групп товаров» и т. д., и т. п.
Извините, тут я с вами спорить не буду, потому что вы поднимаете вопрос идеалогии. Вам так больше нравится — ок.

Лично мне не кажется, что есть способ написать безопасный, хорошо оптимизированный один endpoint на все случае жизни. По крайнем мере, ни у кого это сделать пока не получилось. Поэтому если я хочу, чтобы мои сервера были безопасными и быстрыми, я готов делать отдельное представление для наборов данных. Кроме того, это еще и довольно дешево. Если внутренние механизмы доступа и работы с данными реализованы грамотно, то создание такого представления занимает отсилы 1-2 дня, потому что это лишь композиция существующего функционала.

Опять же речь не о RPC, где могут существовать методы типа getStatsByCities, речь о том что в архитектуре REST ресурс !== таблица БД, а полностью универсальный сервер можно написать только, если принять это равенством. Самое забавное, что даже в этом случае написать решение на все случае жизни все равно не выйдет.

p/s Еще раз уточню, что исключение могут составлять некие публичные апи, у которых цель работы — обслуживание неопределенного кол-ва клиентов, реализующих неопределенное кол-во задач. Тут наверное, разработчикам просто приходится искать максимально универсальные решения, потому что это часть задачи.

Это не идеология, это голый прагматизм: делать новый ендпоинт или расширять имеющийся только когда фронту нужны новые данные, а не просто новое сочетание тех, которые и так ему уже доступны, пускай и немного в другом виде. При условии, конечно, что реализация универсального (в рамках архитектурной модели) API проще чем реализация 100500 отдельных ендпоинтов на каждый каприз пользователя.


Полностью универсальный сервер точно не получится сделать приняв ресурс === таблица БД, потому что табличные БД и вообще БД имеют свои ограничения.

При условии, конечно, что реализация универсального (в рамках архитектурной модели) API проще чем реализация 100500 отдельных ендпоинтов на каждый каприз пользователя.

Ключевой момент. В том то и дело, что не проще. А уж насколько он не проще в поддержке, это и представить сложно.

Полностью универсальный сервер точно не получится сделать приняв ресурс === таблица БД, потому что табличные БД и вообще БД имеют свои ограничения.

Только так и получится сделать, потому что вы фактически вынуждены маппировать апи на sql (или иной язык запросов), то есть придумываете свой язык запросов поверх http. Что и сделали GQL. Если же вы не примете что ресурс === таблица БД, то в какой-то момент необходимый клиенту запрос будет просто невозможно осуществить с помощью вашего универсального endpoint.

Выставляем наружу не реляционную структуру базы, а объектную (предположим что ООП) модель, которая где-то под капотом маппится на базу посредством ORM.

Ну и какая разница? В ORM обычно модель напрямую связана с конкретной таблицей. Вы таким образом только утилитарные таблицы для связей many-many исключаете из апи. Остальные таблицы по сути станут ресурсами.

Если вас это устраивает, то можете попытать счастье в попытке сделать апи для всех случаев жизни. Из моего опыта — это не возможно.
Напрямую с какой-то таблицей модель связана только в двух паттернах ORM из трёх, причём один из двух очень редко встречается (я лично не встречал ни разу). Собственно суть ORM в том, чтобы развязать объектную и реляционные модели, чтобы о таблицах знал только ORM.
Больше похоже на жанглировние понятиями чем на довод. В итоге Entity ORM, например, Posts, все равно маппируется на таблицу posts и ничего тут с этим не сделаешь.

Этот тред прекращаю, потому что он ушел вообще не туда. Хотите создавать унифицированные апи под любые цели — флаг в руки и удачи. Она понадобиться.

Лично я за максимальный контроль со стороны бекенда и за то, чтобы ресурс представлял данные так, как их «видит» клиент, а не БД.
В целом и первый вариант вполне может удовлетворять изложенным принципам REST и его можно описать как «RESTful API, использующий HTTP в качестве тупого транспорта». То, что излагается дальше можно описать как «RESTful API, использующего все преимущества и семантику HTTP, как REST-протокола „
Кто использует JSON API спецификацию у себя в проекте, как вы решаете проблему загрузки файлов на сервер? Поделитель опытом, пожалуйста.
Эта задача в данный момент не описана в спецификации. Вот тут Вы можете почитать обсуждение и возможные решения этой проблемы.

Мы используем загрузку файлов через Multipart.
Видел это обсуждение. Интересно кто как решает данную задачу у себя. Спасибо!
Ох уж этот сказочный удивительный мир, где над ресурсами существует всего 4 операции.
Спецификация не ограничивает вас 4-мя операциями.

Does JSON:API take any position on URI structure, on rules for custom endpoints, which do not fit the paradigm of GET/POST/PATCH/DELETE on the resource URI, etc.?

JSON:API has no requirements about URI structure, implementations are free to use whatever form they wish.

jsonapi.org/faq/#position-uri-structure-custom-endpoints
В статье (как и в большинстве статей о REST) приводятся примеры правильного REST-сервиса как набор из пяти конкретных действий и эти действия четко определены типом запроса. Слишком наивно для приложения, выходящего за рамки CRUD.

JSON API — это лишь схема описания ресурса и подобного вопроса она конечно же не рассматривает, то, что она не накладывает никаких ограничений на endpoint логично, но ведь концепции REST, описанные в статье — накладывают, ибо путь /articles/1/arhive уже попадает под «Здесь все неправильно относительно всех принципов REST». Ни в одной статье, попадавшейся мне на глаза, об этом не было ни слова, к сожалению.
Да, вы правы, в данной статье не освещены дополнительные операции над ресурсами.

Могу сказать только, что в репозитории спецификации это обсуждалось вот тут. И продолжает обсуждаться в связанных issues (https://github.com/json-api/json-api/issues/745).

POST метод как триггер какой-то операции, особенной асинхронной вполне допустим.

Мне в конце концов удалось найти как выкручиваются люди, использующие JSON API с кастомными действиями над сущностью: en.wikipedia.org/wiki/HATEOAS

В примере по ссылке предлагается указывать дополнительные действия в объекте «links». Думаю, идеальный REST такого все-таки не прощает и по канонам требовался бы специальный тип запроса на каждое действие, но ничего лучше, судя по всему, нет и не будет.

Вполе в духе идеального REST, но вот методы так не документируются.

НЛО прилетело и опубликовало эту надпись здесь
Они используются примерно для одной и той же цели и решают похожие проблемы
Естественно, они похожи в чём-то. :)
А как в JsonAPI решается следующая задача?
  • Есть список каких-то сущностей GET /api/entity
  • Список этот можно фильтровать, например по году, или другой сущность author
  • Нужно в API ответе передать разрешенные значения для фильтрация год (2020, 2015, 2012), author (другие сущности)


Единственный вариант, который я вижу это самому как-то руками засунуть данные в meta, но выглядит не красиво :(

Да, это либо meta-информация, т.е. данные о данных.
Либо можно указать ссылки на куски данных с этими фильтрами через links.


Либо описать допустимые фильтры в документации к API.


Ещё можно выдавать ошибку при использовании "неразрешённых" фильтров с указанием "разрешённых".

Зарегистрируйтесь на Хабре, чтобы оставить комментарий