Pull to refresh

Comments 255

Сломано копий и разбито щитов в баталиях славных! Поднимем же кубки за тех героев отважных, осмелившихся бросить вызов богам по протоколу HTTP!
Понапридумано и понаобсуждено и понахоливарено столько вокруг старичка, что каждый для себя сам может выбрать удобный подход для своего вполне конкретного проекта.
А ложки нет.

Стоит добавить, что JSON RPC 2.0 — это транспорто-независимый протокол. Т.е. как именно клиент передаст запрос серверу — никем не регулируется, можно хоть в файл /tmp/request.json его записать, а ответ считать из созданного сервером файла /tmp/response.json. Можно передавать по TCP as is (хотя нередко используют в качестве разделителя запросов/ответов \n если неудобно использовать потоковые парсеры JSON). Удобно передавать отдельными сообщениями WebSocket. Что касается самого распространённого случая — HTTP — то поскольку есть множество способов передать кусок JSON через HTTP то был дополнительно написан стандарт JSON-RPC 2.0 Transport: HTTP, и обычно все клиенты/сервера JSON RPC 2.0 использующие в качестве транспорта HTTP пишутся в соответствии с ним.

Yet another standart, или еще один способ отказаться от простых и понятных систем мониторинга.
Меня это постоянно удивляет. Если уж используется HTTP для транспорта, то почему он используется специально целенаправленно в обрезанном виде? Invalid Request с ответом 200.

Потому что как транспорт — он свою задачу выполнил корректно, поэтому и 200. Попытка для ошибок подобрать какой-то другой код обречена на неудачу в любом случае, по причине того, что в RPC возможных кодов и типов ошибок намного больше, чем в HTTP, и они разные в каждом сервисе. Плюс есть batch-ответы, в которых возвращается массив обычных ответов, часть из которых может быть ошибками. Ну и вообще смешивать уровень транспорта и приложения без веской причины плохая идея. По сумме всех этих факторов получается, что если возвращать что-то помимо 200 то это создаст больше проблем, чем решит.


Что до мониторинга, то RPC-сервисы обычно реализуют более сложные операции (по сравнению с настоящим REST), и для их мониторинга 200/400/500 всё-равно не хватит, поэтому мониторинг таких сервисов обычно реализуется через их собственные метрики уровня приложения, собираемые prometheus.

Я не говорю, что надо для одних ошибок использовать HTTP коды, а для других писать в JSON.
Я говорю о том, что не нужно отказываться от HTTP кодов из-за мониторинга.
В мониторинге сразу будет видно, что пошел шквал 403 или 404, к примеру. Травить монитор на json так же надо, но я все равно честно не понимаю зачем отказываться еще от одного стэка мониторов и анализаторов. При чем отказываться целенаправленно.

В одном ответе от RPC-сервера может быть несколько разных ошибок.
Например, "Access Denied" + "Method Not Found". А HTTP-код у нас только один.

Да. Для пакетных операций надо хорошенько подумать. Согласен.
А если не на пакетную операцию возвращается несколько кодов, но я бы хорошо подумал над другими вопросами.
Может, просто зря «пакетирование» затащили на этот уровень протокола?

Сделали бы отдельный слой для пакетирования.
UFO just landed and posted this here
Что мешает использовать его для всего остального?
А еще в одном ответе на запрос «лайкнуть» может быть сразу несколько ошибок по сущностям, ведь мы можем обновить / создать несколько сущностей. А если не возвращать эти ошибки, а только отвечать «не удалось» в силу, например, секуьюрности, то на кой нам тогда вообще тут REST, как сущность-ориентированный подход? RPC в руки любого типа и вперед. можно и JSON-RPC. Я видел как люди через ПХП с сеть строили сервисы с бинарными протоколами, после этого любое решение кажется вменяемым)))

В переложении на REST может быть не сразу очевидно. Вот классная статья по теме How to GET a Cup of Coffee. Если нужны согласованные изменения, создавайте транзакци, например используя Two-phase commit protocol .

Какой вариант two-phase protocol использовать, самописный? Под это дело нужна дополнительная инфраструктура, это увеличивает издержки в разработке и поддержке.

справедливости ради, даже когда возвращается не-2хх — транспорт свою работу все равно выполнил корректно.

В случае JSON RPC 2.0 — нет. Случаи не 2xx — это случаи когда транспорт свою работу не выполнил — переданный клиентом кусок JSON не был доставлен RPC-сервису.


А в случае REST — большинство не 2xx действительно не ошибки транспорта (ошибками транспорта являются обычно 502/503/504).

в таком случае вы считаете http транспортом между rpc-сервисами?
Мне кажется, вы наделили не свойственной ему ролью.

Я не "считаю" его транспортом, я (и не только я) вполне осознанно использую очень богатый и функциональный протокол HTTP в качестве тупого транспорта, игнорируя практически все его возможности. :)

вы не поняли. http — это транспорт гипертекста. Букв. Конкретно rpc-сервис — штука более специфичная. У него другие состояния, другие возможные ошибки. Если http — тупо транспорт — не-2хх — это не ошибки транспорта. А если это транспорт rpc-сервиса, а не просто букв — вот тут и появляется несвойственная ему роль.

В этом случае HTTP не используется как "транспорт RPC", он используется как "транспорт чёрного ящика в формате JSON, в режиме запрос-ответ".

Yet another standart, или еще один способ отказаться от простых и понятных систем мониторинга.
Меня это постоянно удивляет. Если уж используется HTTP для транспорта, то почему он используется специально целенаправленно в обрезанном виде? Invalid Request с ответом 200.

потому что HTTP для jsonrpc yet another transport
Дак если HTTP и так используется, то почему бы его возможности так же не учесть?
Простые и понятные системы мониторинга продолжат работать, мониторя, например, обращения не к ендпоинту, обращения с «левыми» content-type, падения апп-сервера и прочее, что относится к http.
Если уж используется HTTP для транспорта, то почему он используется специально целенаправленно в обрезанном виде? Invalid Request с ответом 200

Если мы говорим о веб-браузерах, то некоторые коды ошибок они, заразы, обрабатывают самостоятельно, просто не допуская до них наше прикладное ПО.
Я бы не стал полагаться на разнообразие кодов ошибок HTTP, за небольшим исключением.
если отвязаться от передачи информации через сам протокол (http error codes), то в дальнейшем будет легко перейти на вебсокеты.
Лучше всего с CQRS работает GraphQL. Фактически, он под него и моделировался.
На старой работе я пилил все сервисы на JSON-RPC, а потом пришел новый чувак, у которого его свежий PhD давил на мозг, и просто измучал меня тем, что я не делаю все по канонам REST и его пророков.
У меня такое ощущение, что этот чувак пришел к тебе из моей организации. Сколько я всего наслушался, возвращая код 200 с телом error.
А можно узнать почему вас заставляли возвращать 200? Какая была аргументация если есть довольно большой диапазон 4хх и 5хх и прочие? Чем аргументировали те кто продавливал такую имплементацию?
Ну например тем, что запрос отработал и штатно возвращает ответ. То, что в ответе ошибка — это уже дело программы, а не протокола. Эта же тема обсуждается выше в начале комментариев.
Аргументация в данном случае простая и ее уже писали выше: «Потому что как транспорт — он свою задачу выполнил корректно, поэтому и 200». Если мы пытаемся отправить запрос по адресу «site.com/api/v1», а такой адрес отсутствует, то тогда возвращаем 404, или нет доступа – 403, возможно сервер упал в момент обработки и не смог сформировать ответ – 500. Но если сервер получил запрос и, исходя из бизнес-логики, решил что он ошибочный (например: объект не найден), то в таком случае я считаю правильно ответить кодом 200, а вот уже в JSON-ответе в поле «error» указать код и описание ошибки.
Согласен с вами.
На самом деле раздражает очень сильно меня REST уже. Я работал с довольно большим количеством различных API и меня от натягивния совы на глобус уже начинает подташнивать:
— Внезапно, появляющиеся ошибки в теле ответа, в некоторых местах (причем в документации ни слова)
— Натягивание HTTP кодов на совершенно не подходящие им случаи
— GET для получения информации (который очень меня веселит когда у ребят ограничение длинны URL)
— Путаница с PATCH/POST/PUT прям в пределах проекта.
— Возврат 500, когда на самом деле 400 (как приятно это отлаживать)
— Возврат 500/400, когда на самом деле 413 (как приятно экспериментально вычислять...)
— Мое любимое: возврат 4xx/5xx без дополнительной информации но при этом частично выполнить запрос.

Я может сильно не прав, но вроде как, всякие клевые штуки должны упрощать всем жизнь а не усложнять.

Кажется, весомая часть ваших претензий не к REST как таковому, а к отсутствию стандарта. Тогда как для REST есть, например, JSON:API https://jsonapi.org/ У этой спецификации, безусловно, есть недостатки, но при использовании проблемы вроде "возврат 4xx/5xx без дополнительной информации но при этом частично выполнить запрос" уже актуальны не будут.

Я, кажется, ничего не говорил про HTTP 1.1

Что в гипотетическом стандарте REST может отличаться от реального стандарта HTTP 1.1?
Я не понимаю зачем нужно натягивать одно на другое?
Ну нужно вернуть ошибку ну пусть будет ERR_CONTENT_TOO_LARGE.
А когда прилетает некая 4xx и ты сидишь и думаешь, это я накосячил в запросе, сервак отлуп по размеру дает или приложение не справляется с таким пакетом? Очень здорово, особенно учитывая всякие прикольные сервисы, вот например концовка запроса одного из: "]]]]}"

Отдельную кстати боль доставляют любители выносить параметры в заголовки, особенно доставляет когда это указано где нить в общем разделе в духе «Если вам нужны только обновленные данные используйте if-modified-since».

В чем принципиальная разница между JSON:API и JSON:RPC?

"JSON:RPC" не существует. Есть конкретный протокол "JSON-RPC 1.0" (устаревший и почти не использующийся) и конкретный протокол "JSON-RPC 2.0" (текущий, обычно говоря о "JSON RPC" имеют в виду именно его, но не обязательно — иногда речь о своей кустарной реализации RPC с использованием JSON для запросов/ответов).


"JSON:API" это тоже конкретный протокол. Не уверен насчёт его природы на 100% (я его смотрел поверхностно), но по-моему он намного ближе к REST/GraphQL, и полноценным RPC не является. Начинался он как REST-адаптер для фреймворка Ember.js, который потом решили попытаться отвязать от Ember.js и превратить в стандарт. На данный момент, по моему личному мнению, преимуществ перед JSON-RPC/REST/GraphQL у него нет, если только мы не пишем проект на Ember.js, а вот неприятные проблемы присутствуют (почитайте его форум).


Плюс к этому "JSON API" часто называют кустарные варианты API с передачей запросов/ответов в JSON — они могут быть по природе похожи на REST, могут быть похожи на RPC, могут быть вообще чем угодно.

На данный момент, по моему личному мнению, преимуществ перед JSON-RPC/REST/GraphQL у него нет

Преимущества JSON:API перед JSON-RPC те же самые, что и у REST перед JSON-RPC (как и недостатки)


Преимущества перед REST:


  • стандартизация некоторых холиварных моментов (таких, как пагинация, HTTP-статусы ответов в большинстве случаев)
  • возможность выборки только нужных полей сущности
  • возможность получить только нужные связанные данные

Преимущества перед GraphQL:


  • отсутствие повторяющихся данных (если мы выбираем N статей с их авторами, в GraphQL для каждой статьи продублируется автор, даже если он один, а JSON:API вернет только уникальных авторов)

Ну так REST это по сути и есть "JSON API", с рекомендациями оперировать понятиями "ресурс", "операция над ресурсом" и использовать при этом возможности протокола HTTP согласованным образом.

Суть REST не имеет вообще никакого отношения к JSON, так что не может быть "JSON API".

Я говорю о типовой реализации REST-сервисов, а не об абстрактной сути. У вас в REST-сервисах не используется JSON как контейнер для полезной нагрузки?

У нас вот универсальный, по HTTP заголовкам определяет как парсить тело и что отдавать. JSON лишь один из поддерживаемых типов.
Но если сервер получил запрос и, исходя из бизнес-логики, решил что он ошибочный (например: объект не найден), то в таком случае я считаю правильно ответить кодом 200

Почему тогда не 400 (или 404 если ID объекта это часть урлы)?
В JSON-RPC id объекта это не часть урлы. Id объекта передается в теле в поле/ключе params.
Один и тот же код ошибки может означать совершенно разные вещи. При не очень аккуратной настройке веб-сервера можно получить почти любой код ошибки при любом запросе — а потому кодам ошибок тяжело доверять.

Так, 403 может означать как ошибку авторизации, так и ошибку в адресе. А ещё 403 может означать что сервис временно недоступен и надо повторить запрос позже.

Код 401 может означать как необходимость передать токен — так и ошибку в адресе или что сервис недоступен и нужно повторить запрос позже.

Код 404 может означать некорректный id, ошибку в адресе или недоступность сервиса.

Код 405 может означать как ошибку в алгоритме — так и ошибку в адресе или недоступность сервиса.

Код 418 может означать что угодно (кроме ошибки в адресе или недоступности сервиса).

Коды 502 или 504 могут означить что угодно, включая ошибку в адресе или недоступность сервиса.

Код 503 может означать временную недоступность сервиса… или ошибку в адресе.
Есть один маленький ньюанс — с сессиями в куках (и вообще чем угодно в куках). Тут тоже можно вдоволь похоливарить. По-идее, канонично будет не использовать HTTP-заголовки. Но в этом случае нужно хранить состояние на клиенте и явно передавать с каждым запросом. Т.е. то что раньше было простым GET или POST запросом авторизованным юзером (без JS) теперь придется заворачивать в POST (с обязательным использованием JS для чтения из локального хранилища).
По моему, авторизация — задача не бизнеслогики, отвечающей на запрос. Авторизация должна быть между веб-сервером и сервером БЛ. HTTP-заголовки, сессии, токены никто не отменял. Это и в стандарте JSON-RPC 2.0 Transport: HTTP упомянуто.
Простите, но ссылка на proposal\draft на домене simple-is-better никак не тянет на стандарт, оригинал если что тут: www.jsonrpc.org/specification, и там ни слова про заголовки и токены (что ожидаемо, ведь JSON-RPC не должен зависеть от сторонних протоколов и быть максимально переносим).

Про авторизацию в корне не согласен, она является частью именно бизнес-логики приложения, не очень понятно что вы имели ввиду говоря «должна быть между веб-сервером и сервером БЛ».

Альтернативы ей нет, все кто активно используют JSON RPC 2.0 про неё знают, поэтому хоть у неё и нет официального статуса де-юре, но де-факто это стандарт.


Авторизацию/токены обычно передают отдельным первым параметром (в нём так же удобно передавать дополнительные данные для трассировки запросов, отладки, в общем — контекст запроса) во все RPC-методы, т.е. на уровне HTTP не используются ни заголовки ни куки. Но, да, если на уровне приложения авторизации нет, то можно прикрыть весь сервис на уровне стоящего перед ним nginx и проверять заголовок Authorization:, спека это не запрещает.

В стандарте JSON-RPC ничего подобного и не должно быть упомянуто, поскольку это просто не тот уровень стека протоколов.
Про «стандарт» это я дал маху, согласен. С другой стороны стандарт — дело личное, можно за стандарт для себя/для компании принять то, что на заборе написано.

Про авторизацию: метод, обрабатывающий JSON-RPC запрос — это функция, которая принимает на вход то, что пришло в поле «params» и возвращает ответ — это бизнес-логика.
Позволить выполнить функцию или нет — задача системы авторизации. Она может быть реализована декоратором функции/класса или веб-прокси или ещё кучей других способов.
«Простой POST-запрос авторизованным юзером» нужно обязательно защищать анти-CSRF токеном, иначе это будет дырка в безопасности. А наличие такого токена, в свою очередь, сделает использование API не из браузера не таким простым как хотелось бы.

Защита от CSRF для JSON RPC 2.0 обычно не нужна — потому что куки не используются, авторизация реализована через передачу отдельного параметра с токеном, который стороннему сайту взять негде. В качестве вишенки API зачастую не публичный, открыт только для нашего сайта, а спека требует передать Content-Type: application/json, который не является simple header и активирует CORS, что дополнительно расширяет возможности сервера по фильтрации левых запросов (напр. если для авторизации используется заголовок Authorization:, но это редкость).

Так я о том и говорю! Куки обычно не используются — именно по названным мною причинам.

Нет, куки не используются потому, что проверкой аутентификации и авторизации обычно в RPC занимается слой бизнес-логики (точнее, обёртка перед ним, но это не важно — она всё-равно находится внутри RPC-сервера). А это значит, что все необходимые для проверки данные (токен) они должны получить вместе с методом и параметрами RPC. Информация с транспортного уровня (HTTP) в этот момент обычно недоступна (если совсем-совсем честно, то обычно приходится изворачиваться, чтобы дать доступ к одному значению с транспортного уровня: IP клиента, для защиты от DoS). Более того, JSON RPC 2.0 не просто "формально" независим от транспорта, он и на практике часто передаётся по TCP или WebSocket, где никаких кук нет. В общем, куки не используются не ради защиты от CSRF, а потому, что они вообще никак в JSON RPC 2.0 не лепятся.

«Информация с транспортного уровня (HTTP)» — это в какой модели он вдруг транспортный?

Для JSON RPC 2.0 — он вполне себе транспортный. Потому что всё, что от него требуется — доставить кусок JSON туда и обратно. Никакие особенности HTTP при этом не нужны и не используются (в отличие от того же REST), никакие метаданные к этому куску JSON не прилагаются, etc. — стандарт рекомендует использовать конкретные методы (POST) и заголовки (Accept/Content-Type: application/json) исключительно ради интеоперабельности с существующими HTTP-сервисами вроде прокси, самому JSON RPC 2.0 всё это не нужно.

В примерах к apidocjs только REST однако

А вот это да. Инструментов для описания и документирования API на базе JSON-RPC похоже нет. Он слишком стар для всего этого. Когда начался подъём популярности JS, REST захватил умы большинства, и инструменты пишут для него. Сейчас ещё GraphQL добавился. А JSON-RPC несправедливо забыт.

Возможно и не нужно дополнительных инструментов. Ендпойнт один (какой-нибудь example.com/api/), запросы всегда POST (если следовать стандарту). Нужно лишь описать параметры методов и возвращаемый ими результат. С этим справятся jsdoc, pydoc и прочие подобные.

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

Периодически мониторю интернет в поисках такого инструмента, но всё тщетно :)
Не то что уровня swagger, вообще ничего нет! Видимо нет запроса от аудитории, все ушли в REST.
UFO just landed and posted this here
UFO just landed and posted this here
что значит выстрелить, давно и успешно используется

А чем конкретно не нравится?

Извините, что встреваю. У меня есть к нему претензия. Я читал код grpc-либы под python и моя жизнь никогда не будет прежней (к слову о «гениях», работающих в Google). И сам protobuf — не самый приятный IDL из существующих. Однако, по моему опыту это лучший инструмент для кроссплатформенного RPC поверх http2, который распространен чуть более, чем везде. Двухсторонние стримы, мультиплексирование (куча виртуальных коннектов в рамках одного tcp-соединения), сжатие, поддержка практически всех ходовых платформ… Плюс вменяемая документация в виде protobuf-описаний. В общем, хоть он и горький, но лекарство после «кто-в-лес-кто-по-дрова-REST-это-не-спецификация-а-общий-набор-рекомендаций» и неэффективного json-rpc, для которого кроме спецификаций полей простейшего request-reply протокола вообще ничего толком нет.

Претензия к реализации не самая валидная. Да, чистый код это очень важно, но если нет выбора, то если реализация качественно протестирована, работает без проблем, то что у неё внутри — становится не критично, коль уж лично Вам лезть в этот код не придётся.

UFO just landed and posted this here
Инструменты есть, но мало о них кто знает. В 2012 писал про SMD схему habr.com/ru/post/150803 (ctrl+f smd), Недавно нам удалось сделать github.com/semrush/smdbox, хорошо работает как с PHP реализацией github.com/sergeyfast/eazy-jsonrpc/tree/shuler_response, так и с Go github.com/semrush/zenrpc. Т.е. есть автодокументация и playground.

Так же есть еще генерация сваггера (все в репозитории выше), пример api.myshows.me/shared/doc, пример endpoint'a api.myshows.me/v2/rpc/?smd

Удалось сделать даже документацию ошибок, которые есть в JSON-RPC. Ну и генерация самого клиента тоже есть. Вот так это все примерно выглядит (smdbox)
image
Выглядит юзабельно! К сожалению, плюсик не могу поставить
UFO just landed and posted this here
UFO just landed and posted this here
Я тоже не понимаю, можете коротко развернуть?
UFO just landed and posted this here
Если бы REST был стандратом, там было бы написано как делать надо. Видимо не написано?
UFO just landed and posted this here
Во-вторых, кодов ответа в HTTP всегда меньше, чем типов ошибок бизнес-логики, которые вы бы хотели возвращать на клиент.
Кто-то всегда возвращает 200-ку, кто-то ломает голову, пытаясь сопоставить ошибки с HTTP-кодами.
В JSON-RPC весь диапазон integer — ваш.

наверное, опять холиварная тема, но в некоторых случаях предпочел оставить поле error для ошибок протокола, описанных в спецификации, а ошибки бизнес логики передавать внутри result. Некоторые случаи — это api для межсервисного взаимодействия, где сервис одновременно может поддерживать jsonrpc, grpc, очереди сообщений и не только… хочется минимизировать влияние протокола на форматы сообщейни.

Да и нужен ли Вам JSON?


Былой человекочитаемости уж не сыщешь. Эти анонимные объекты кого угодно загонят на перекур!


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


То ли дело XML-RPC! Все чётко, по делу, имя не понятное — глянь схему. Строгость, стройность, либы для всех языков, автовалидация, XPath, — все дела!
Да и стандартище! Не в пример доморощенным костылям.


Бросайте эти ваши ресты с джэйсонами, назад к истокам!

UFO just landed and posted this here
Уж очень он многословный, а мобильный интернет не везде быстрый и бесплатный. Это еще не говоря про "«простой» протокол доступа к объектам" комплектующийся в качестве бонуса болью, отчаяньем и безысходностью.
UFO just landed and posted this here
{«jsonrpc»: «2.0», «method»: «post.like», «params»: {«post»: «12345»}, «id»: 1}

Вот тут сразу виден недостаток стандарта (или следствие наличия «бонуса» в виде batch-операций):

метод закодирован в теле сообщения!

С точки зрения простоты протоколирования, мониторинга, возможности перекрытия реализации метода, балансировки и т.п. это неудобно.

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

А здесь «бонус» вильнул собакой.

Нет, это следствие не batch, а того, что протокол транспорто-независимый, поэтому никаких "путей" (равно как и HTTP в принципе) в нём просто нет.

{«jsonrpc»:«2.0»,«error»:{«code»:1234,«message»:«Server not found»},«id»: 2}

И еще. Почему код — это число, а не строка? Как так?
А почему нет? Боитесь, что чисел для всех ситуаций не хватит? :)
А почему нет? Боитесь, что чисел для всех ситуаций не хватит? :)

Ну хорошо, тогда дополнительный вопрос.

Если «код» сделали числом, то почему «метод» тоже не сделали числом?
Боятся, что что чисел для идентификации методов не хватит? :)
А как связаны код и метод? Зачем делать методом числом или зачем делать код строкой?
А как связаны код и метод?

И код, и метод — это перечисляемые типы.

Представляя значение кода/метода в виде числа мы получаем последствия:

1) разрабатывая API командой (разными людьми) гораздо вероятнее получить коллизию по их значениям;

2) нужно что-то особое придумывать, что бы исправить существующие коллизии и что делать, что бы таких коллизий не было в дальнейшем;

3) представляя значение перечисляемого типа в виде числа мы автоматически попадаем на антипаттерн магическое число.
Как по мне, то вероятности коллизий одинаковы, что в строке, что в числах. Вернее нет оснований считать, что с числами она заметно больше будет. В коде всё равно будут использоваться enum типы или, хотя бы, константы для каждого кода. И с кодами гораздо проще выделять классы ошибок. Собственно как в HTTP.
коде всё равно будут использоваться enum типы или, хотя бы, константы для каждого кода

Эти константы все равно будут иметь какие-то значения. Какая разница, оборачиваются они в красивые идентификаторы или нет в языке программирования?

И с кодами гораздо проще выделять классы ошибок.

Точно не проще. Потому что каждое число и так имеет строковое представление.

А что бы изолировать набор значений, достаточно придумать уникальный префикс. С числами такой фокус не проходит.
UFO just landed and posted this here
Реальный мир шире, чем HTTP-протокол.

Ради чего ограничивать себя сотней типов (значений)?
Как это решит проблему коллизий?

Это что-то из разряда «640 кб хватит всем»?
Что бы «предполагаемый противник» не догадался о значении ошибки, особенно если в message писать одно и то-же для всех ошибок :)
1234 — это 1234 на любом языке, а message можно отдавать на языке пользователя.
Ещё числовой код удобно бить на диапазоны по критичности, например, или области ответственности.
а message можно отдавать на языке пользователя

А как API узнает язык пользователя?

И какого пользователя? Оператора или программиста?

И зачем, вообще, служебные сообщения переводить?
А как API узнает язык пользователя?
Из заголовка Accept-Language, например.
И зачем, вообще, служебные сообщения переводить?
Если у вас мультиязычное мобильное приложение, то вполне допустимый вариант переводить их на стороне API.
Из заголовка Accept-Language, например.

Во первых, у нас же протокол «изолирован» от HTTP, какие заголовки?
Во вторых, ради чего эти усложнения, делать вам больше нечего?

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

Мой идеал: API занимается обработкой данных (доступом к данным), мобильное приложение — представлением этих данных, в т.ч. локализацией. Каждый в чужую зону ответственности не лезет.

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

Банально, данные в трёх экземплярах не будешь хранить локально. Локализация вполне себе может быть "на лету".

На практике наиболее оптимальна комбинация server-side/client-side localization&internationalizationдля разной степени динамичности элементов, пропускной способности канала и профиля взаимодействия с фронтендом (частота опрашивания, количество перед. данных, кол-во открываемых соединений, вид транспорта и т.д.)

Статику вполне допустимо локализовывать client-side и отдавать в API только string ID, а вот часто обновляемый контент удобней локализовывать server-side. Также для многих языков есть свои разные устоявшиеся практики/рекомендации на эту тему.
Нет, это следствие не batch, а того, что протокол транспорто-независимый, поэтому никаких «путей» (равно как и HTTP в принципе) в нём просто нет.
Вопрос от человека незнакомого с протоколом: не повторяют ли разработчики структуру HTTP путей? То есть, в REST у нас имеется post/like, post/bookmark, post/share/vk, etc. Не будет ли в json rpc построена аналогичная структура: «method»:«post.like», «method»:«post.bookmark», «method»:«post.share.vk»?
Будет что-то типа Api.LikePost, Api.BookmarkPost, Api.SharePost.
В jsonrpc нет путей, есть нотация Service.Method. На одном эндпоинте может быть несколько сервисов.

Но самое забавное, что даже в этом примере, который должен показать типо независимость и самостоятельность json-rpc, автор по сути придумал yet another post request.


Имхо не надо придумывать миллион велосипедов в себе. Универсального все равно не будет! И не надо добиваться бюрократической точности в соблюдении стандартов без необходимости. В 95% случаев совет — юзать рестоподобную архитектуру, хорошо документируйте ее и никогда ни у кого не возникнет проблем с пониманием вашего велосипеда.
GameDeV + Realtime = http rest???? Не смешите мои тапочки. И джейсон-рпц сам по себе тут проблем не решает. Имхо тут важнее используемый транспортный протокол, чем структура данных, которую вы передаете. А передаете вы джейсон, xml или двоичные данные — вопрос уже десятый.

Да, тут подход как у SOAP. Как по мне REST гораздо интуитивные.
Он интуитивен пока модель за API хорошо ложится на концепцию обмена представлениями ресурсов, грубо говоря на CRUD модель.

Вспоминая о JSON-RPC и документировании API, не забывайте о SMD.


Например
{
  "transport": "POST",
  "envelope": "JSON-RPC-2.0",
  "contentType": "application/json",
  "SMDVersion": "2.0",
  "services": {
    "post.list": {
      "transport": "POST",
      "envelope": "JSON-RPC-2.0",
      "parameters": [
        {
          "name": "author",
          "type": "string"
        },
        {
          "name": "token",
          "type": "string",
          "optional": true
        }
      ],
      "returns": {
        "type": "array",
        "items": {
          "type": "object",
          "properties": {
            "title": {
              "type": "string"
            },
            "content": {
              "type": "string"
            },
            "author": {
              "type": "string"
            },
            "status": {
              "type": "string",
              "enum": [
                "draft",
                "published"
              ]
            }
          },
          "required": [
            "title",
            "content",
            "status",
            "author"
          ]
        }
      },
      "post.create": {
        "transport": "POST",
        "envelope": "JSON-RPC-2.0",
        "returns": {
          "type": "object",
          "properties": {
            "title": {
              "type": "string"
            },
            "content": {
              "type": "string"
            },
            "author": {
              "type": "string"
            },
            "status": {
              "type": "string",
              "enum": [
                "draft",
                "published"
              ]
            }
          },
          "required": [
            "title",
            "content",
            "status",
            "author"
          ]
        },
        "parameters": [
          {
            "name": "title",
            "type": "string"
          },
          {
            "name": "content",
            "type": "string"
          },
          {
            "name": "status",
            "type": "string"
          },
          {
            "name": "token",
            "type": "string"
          }
        ]
      }
    }
  }
}

При желании можно и какой-нибудь UI оформить, наподобии сваггера

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

Как здесь будет выглядеть с description?


{
    // url for special service
    // "serviceUrl": "http://example.com/api",
    // change back when standard url will work
    "serviceUrl": "http://subdomain.example.com/api"
}
Можно было бы использовать JSON5, который допускает использование комментариев, если SMD его поддерживал.

Не очень понял вопрос. здесь — это какое место?


{
    "serviceUrl": "http://subdomain.example.com/api",
    "description": "change back when standard url will work"
}

Вот так не получится?

Все закомментированное с сохранением информации. Для отладки тоже часто бывает нужно, например временно переключить БД по умолчанию на свою локальную.
{"description":"url for special service\n\"serviceUrl\": \"http:\/\/example.com\/api\"\nchange back when standard url will work"}

Но это будет приемлемо, если использовать какой-нибудь UI для SMD. В чистом виде читать это — такое себе, да...


А что касается БД — то каким образом тут SMD играет роль?
SMD — это описание, а БД — это уже реализация...

Ну так параметры подключения к БД хранятся в конфиге. Надо переключить, закомментировал одну строчку, раскомментировал другую.

Как-то меня смущает во всей этой холиварне слово "stateless". На большинстве сайтов, с которыми лично я сталкиваюсь, требуется аутентификация/авторизация, а это уже, как минимум, два состояния: анонимный и аутентифицированный.

Потому что вы не правильно понимаете это слово. Под «состоянием» имеется ввиду состояние некой «сессии» между клиентом и сервером. Например такой «сессией» может быть подключение через TCP. Как только оно разрывается, то «сессия» завершается, состояние теряется. При следующем подключении надо это состояние опять восстанавливать: выполнять аутентификацию, давать команды для перехода в нужную директорию (как в FTP) и др.
Если же у вас «stateless» — то каждый запрос от клиента передаёт всю необходимую информацию не полагаясь на то, что сервер «помнит» клиента. В общем случае запросы клиента могут с помощью балансировщика направляться на разные сервера, какая тогда вообще может быть речь про «сессию» и «состояние сессии»?
В общем случае запросы клиента могут с помощью балансировщика направляться на разные сервера

Вот и вопрос, как же тогда разные сервера определяют, анонимный клиент дал запрос или он уже аутентифицирован ранее и имеет права на доступ к restricted-данным?

Ну если у разных серверов есть доступ к общей базе с этими «restricted-данными», то скорее всего в этой же (или соседней) базе есть данные необходимые для авторизации клиента. Например есть таблица с мапингом токенов на юзеов. Клиент в этом случае передаёт в каждом запросе свой авторизационный токен, который получил от сервера в процессе аутентификации.
Ну или всё ещё проще — клиент в каждом запросе передаёт свой логин-пароль, как это сделано в HTTP Basic авторизации.
Филдинг описал концепцию построения распределённого приложения, при которой каждый запрос (REST-запрос) клиента к серверу содержит в себе исчерпывающую информацию о желаемом ответе сервера (желаемом представительном состоянии), и сервер не обязан сохранять информацию о состоянии клиента («клиентской сессии»).

Это из вики. Получается, что таблица с авторизационными токенами на сервере — это не REST. Если подходить строго, то REST возможен только при HTTP Basic аутентификации. Только тогда сам запрос содержит в себе достаточно информации, чтобы сформировать ответ. При этом на каждый запрос нужно будет проводить аутентификацию и авторизацию.

О! Вот это то самое! Спасибо :)

Но помните, что исключив обращение к БД, вы потеряете возможность мгновенного бана пользователя.
А зачем смешивать аутентификацию (JWT) и авторизацию? Всё равно проверять, есть ли у данного пользователя права на запрошенное действие.
Всё равно проверять, есть ли у данного пользователя права на запрошенное действие.

Можно и не проверять права, а записывать их прямо в токен при выдаче. Если срок жизни токена небольшой (например, минута, цифра с потолка) — мгновенно забанить пользователя, конечно, не выйдет, но после протухания его токен превратится в тыкву, а новый ему уже никто не даст.

Можно в простых случаях, например, когда достаточно прав «guest, user, admin».
В более сложных, когда у пользователя есть права только на определённые действия только для определённых групп объектов токен слишком большой получится.
Ни в коем разе! Аутентификация != авторизация.

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

Разве в качестве аутентификации в JWT выступает не тот факт, что подпись токена верна?
Тогда в качестве авторизации можно использовать содержимое токена без дополнительных обращений к сервису авторизации для проверки того, какие права имеет пользователь такой-то.


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

Ну да. Мы вернулись к тому, с чего начали: так можно жить, но мы лишаемся возможности моментального изменения данных пользователя / бана / разлогина. Каким-то проектам эта схема подходит, каким-то — нет.
Таблица (или полноценный сервис) с токенами вполне может быть за REST ендпоинтом, главное сервер не должен делать предположений откуда получен токен клиентом, не должен предполагать что для получения токена был сделан конкретный запрос перед текущим и какая-то информация о нём сохранилась.
Уже ответили, но я дополню.

Как я написал в первом ответе, наиболее типичным вариантом «сессией с состоянием» как правило является нечто вроде привязки состояния к TCP-соедниению между клиентом и сервером, а не то что у вас в базе данных на сервере или в клиенте хранится. Храните в базе что хотите и как хотите, передавайте информацию от клиента к серверу тоже как хотите (через куки, через тело запроса, через другие заголовки) — главное передавайте в каждом запросе если они ему нужны.

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

Попробую привести «кухонный пример» почему stateless важен для построения масштабируемой архитектуры.
Представьте кинотеатр, кассы, билетёр на входе в зал. Вы купили билет, показали билетёру и вошли в зал. Потом решили вернуться в машину за забытыми очками. Если, когда вы возвращались, билетёр ОБЯЗАН запомнил вас в лицо и пустить без проверки билета — это не масштабируемая система. Такого билетёра нельзя подменить другим, потому что он вас не знает и не сможет впустить. Вам придётся снова проходить процедуру покупки билета, если вы его потеряли, и показывать его билетёру.
Если же билетёр ОБЯЗАН пропускать только по билету — то любой билетёр сможет пропустить вас с билетом, даже если видит вас впервые. Вот это масштабируемая схема — можно поставить хоть 10 билетёров и вы можете к любому подойти со своим билетом, а не только к тому, кто вас первым проверил.
Как вариант, аутентифицированный добавляет к запросу токен, а сервер проверяет валидный токен или нет. Вот только проверяет он не «аутентифицирован ранее», а именно валидный токен или нет.

Что значит в данном контексте "валидный токен"? Может ли у одного и того же пользователя (с одним и тем же внутренним идентификатором на сервере) быть два разных валидных токена? Что делать, если пользователь "засветил" свой токен перед злоумышленником и теперь хочет поменять его?

Валидный значит сервер не видит причин не доверять информации в токене. Может или не может быть несколько токенов, принудительная инвалидация и т. п. — как требования будут, так и реализуете.
Токен формируется в процессе логина как результат успешной аутентификации. Токен нигде хранить не нужно — его достаточно подписать и отдать пользователю. При последующих запросах проверяем валидность переданного нам токена. Если токен попадёт в руки злоумышленника то проблема решается так же как и в случае с паролем — смена пароля ведёт к генерации нового ключа для токенов. Все прежде выпущенные токены становятся не валидными.
Практически, это холиварное слово означает что никаких проблем часть запросов делать анонимно, часть — от имени некоторого пользователя, а часть — от имени другого пользователя, и всё это одновременно. Или последовательно, но через общее HTTP-соединение. Ну, если аутентификация сделана не куками и сессиями, как тут предлагали выше, а как полагается — токенами.

Но токен же должен храниться и на клиенте, и на сервере, чтобы можно было обеспечить аутентификацию/авторизацию? И по мне, так нет никакой принципиальной разницы с точки зрения stateless, как передавать токен с клиента на сервер — в заголовке Cookie или в заголовке Authorization.

Не должен. Токен может быть самодостаточным, содержать, например, userId непосредственно.
Но как его тогда защитить от подделки?
электронной подписью, например
но возникает вопрос — как тогда экспайрить такие токены?
UFO just landed and posted this here
Кроме срока действия может быть обращение к сервису авторизации на предмет не отозван ли он.

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


ИМХО более удачное решение в этом смысле — короткие по времени действия токены.

Состояние состоянию рознь. А ради кейсов, которые происходят раз в полгода, всех клиентов заставлять постоянно рефрешить токен как-то не тру.
У каждого юзера свой криптоключ для подписи токена. Если один из токенов скомпрометирован — меняем ключ и все токены автоматом становятся невалидные. Пользователю нужно заново логинитьcя дабы получить валидный токен.
Ничем не лучше варианта с хранимым списком токенов — точно так же серверу нужна актуальная база с некоторой информацией.
Только непонятно зачем этот список хранить. Совсем непонятно. Этож не сессии.
А как вы собрались узнавать актуальный ключ пользователя чтобы проверить токен?
Так у пользователя всего один ключ актуальный.
Ну тут два пути:
1. Можно всех пользователей по очереди перебирать с их ключами и к какому подойдёт того и пускать в систему.
2. Можно из payload'а извлекать уникальный айди, по нему находить пользователя, получать его ключ и проверять валидность токена. Так все и делают, кстати.
Ну так вам в любом случае нужна актуальная база с ключами пользователей. Чем она принципиально отличается от актуальной базы токенов?

В обоих случаях проверка токена сводится к одному запросу к БД.
Ключ один. А токенов может быть много — для веба, для андроид и иос приложений. У каждого токена может быть своя область видимости а так же время действия. Зачем всё это хранить непонятно. Ключ нужно хранить в любом случае.
В JWT-токен можно положить id пользователя, его имя, список ролей, например. И не надо будет при каждом запросе лезть в базу за этими данными. Хранить только список отозванных токенов (скидывать туда при разлогине или рефреше). Можно держать в памяти (в редисе, например) и проверять хоть nginx'ом на входе. При потере списка невалидных токенов (при ребуте, например) инвалидировать вообще все токены выданные раньше даты ребута, пользователи просто снова залогинятся. Чтобы редис не сожрал всю память, вычищать токены, протухшие по сроку. Вроде схема простая и много где описана.

А в чем принципиальная разница: лезть в базу чтобы проверить что токен там есть, лезть в базу чтобы проверить что токена там нет — или лезть в базе за ключом пользователя?

Слазить в SQL в несколько таблиц, чтоб вытащить инфу о пользователе куда дороже, чем проверить наличие строки в редисе.

А можно чуть подробнее, как сделать, чтобы это проверялось nginx'ом?

Помимо всех плюсов навскидку вижу два минуса по сравнению с REST.


  1. Клиентское кеширование. Можно сколько угодно говорить, что кешировать RPC нельзя (как настаивает указанный выше JSON-RPC 2.0 Transport: HTTP), но в реальности кешировать нужно. И тогда мы либо поддерживаем два разных API — одно JSON-RPC, одно REST или что угодно с кешированием, либо начинаем реализовывать кеширование руками. Изобретаем ETag / Last-Modified, решаем что является идентификатором ресурса, делаем кучу ошибок и не можем пользоваться браузерными кешем.
    Даже в вашем примере Хабра можно кешировать информацию о старых постах, профили пользователя, посты для мобильной версии (куда не включены комментарии), в общем все что редко изменяется. Это позволит сэкономить очень много ресурсов.
  2. Микросервисная архитектура и балансировка нагрузки. На балансировщике намного дешевле проверить префикс URI, чем распарсить JSON и вытащить оттуда метод. Нехорошие клиенты могут отправлять валидные запросы с очень длинным телом, а метод в нем ставить в конце.
    Да даже разделить read-only и read-write становится трудно. Я уже не говорю о том, что при необходимости некоторые параметры можно тоже включить в балансировку нагрузки и в случае REST это все тот же префикс. Да, смешиваем транспортный уровень и уровень приложения, но бывают ситуации когда это оправдано.
    Упомянутый выше мониторинг это проявление той же проблемы под названием "JSON парсить дорого". В приложении это уже не так дорого по сравнению с остальной логикой, но на других уровнях — очень и очень дорого.
В своих проектах я решаю минусы JSON-RPC очень простым способом, для запросов, передающихся через HTTP, просто выношу поле method в URL, строка запроса при этом выглядит как «/api/posts.add», где «posts.add» значение method, а параметры передаю уже в теле через params. Но я не использую batch-операции т.к. в зависимости от метода, балансировщиком выбирается сервер обработки и мешанина методов в одном пакете в этом случае только мешает. Также не использую в теле «jsonrpc:2.0» из-за того что полученный протокол не является чистым JSON-RPC. В остальном же стараюсь придерживаться стандарта.
Наверное, так в основном и делают, а спецификация JSON-RPC не прижилась в жизни:
переусложнена, там, где не надо (из-за batch-операций возникают далеко идущие последствия), сложнее в балансировке и мониторинге относительно REST и полу-REST решений.
Какой ужас. Тут статья про то, какой JSON-RPC 2.0 стандарт, не позволяющий вольностей набрала уже 150 плюсов, а вы jsonrpc:2.0 выкидываете и метод в путь. Ай-ай-ай…
Так поэтому я и написал, что полученный протокол не является чистым JSON-RPC и по этой причине в теле не используется «jsonrpc:2.0». Инструмент надо выбирать из задачи, а не наоборот. В моем случае отсутствует необходимость обращения сторонних серверов к моим API, нет необходимости 100% соответствовать стандарту. Зато этот подход позволяет мне разрабатывать более удобный интерфейс клиент-серверного взаимодействия.

С одной стороны Вы правы. И многие из нас, включая меня, когда-то делали нечто подобное. Но, с годами опыта, обычно приходит понимание, что любое отступление от существующего стандарта/протокола должно требовать значительно больше плюсов, чем "мне так удобнее" или "мы получим одну классную фичу" — потому что неудобств такие отступления от стандартов, со временем, создают больше, чем изначально ожидалось. В качестве конкретного примера таких "неудобств":


  • внезапно бизнес требует обеспечить совместимость со стандартным протоколом, т.к. этого требуют новые партнёры, или это нужно чтобы сделать API публичным, или ещё по куче непредсказуемых заранее причин
  • нужно вручную реализовывать библиотеки для своего протокола, причём нередко со временем оказывается, что нужна не одна для языка используемого сейчас на бэкенде, а ещё одна на бэкенд для другого языка плюс ещё пара для мобильных клиентов плюс ещё одна для веба
  • у стандартных протоколов есть недостатки… как и у любых других, но — недостатки стандартных протоколов уже хорошо известны, документированы, и есть рекомендации по их преодолению… а вот со своим протоколом все шишки придётся набивать самостоятельно, и узнавать о его недостатках в самый неподходящий момент — когда нужно срочно сделать фичу, а протокол начинает в этом мешать, а не помогать
  • следствие предыдущего пункта — свой протокол периодически приходится изменять/расширять не продумав как следует последствия, что создаёт ещё больше проблем в будущем

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

Абсолютно согласен с приведёнными выше пунктами, только они не совсем подходят для конкретно данного случая и протокола, так как:

  • изменения в протоколе минимальны, реализовать публичную точку входа на строгом JSON-RPC будет не очень сложно, и на порядок проще чем в случае с REST
  • сам по себе протокол очень простой и отсутствие специальных библиотек не большая проблема
  • простата протокола позволяет особо не переживать о скрытых ошибках, да и отклонения от стандарта минимальны
  • следствие предыдущего пункта — свой протокол периодически не приходится изменять/расширять не продумав как следует последствия

В данном случае небольшое отклонение от стандарта дает немало ценных преимуществ, компенсирующих вышеупомянутые проблемы.
Если внезапно бизнес требует — то никакого json-rpc там не будет. Там будет либо rest либо soap (прекрасно умирающий, но все еще не умерший) причем с конкретной реализацией протокола обмена поверх rest или soap. Потому что когда у вас интеграция и не вы определяете как она делается можете не рассчитывать на то что там будет хоть-какой-нибудь стандарт.

В некоторой степени вторую проблему можно решить, вынося балансировку на сторону клиента.
Можно сделать API, отдающий конфигурацию вида:


{
  "post.get": "https://posts.example.com/rpc",
  "post.like": "https://likes.example.com/rpc",
  "comment.like": "https://likes.example.com/rpc",

  "default": "https://example.com/rpc"
}

При таком подходе в любой момент времени любой метод можно вынести в отдельный микросервис.

1. Доверять нельзя, как сказали выше.
2. В результате получается тот же URI навыворот.

Кешировать RPC запросы это плохая идея. Нужно кеширование — используйте REST, вместо или вместе с RPC.


Что до балансировки нагрузки на уровне HTTP, то это вообще не проблема — просто делаем для каждого публичного микросервиса свой url/endpoint, вроде https://api.example.com/rpc/microserviceA, и дальше nginx по url раскидает запросы по разным сервисам. Даже если в будущем этот микросервис будет заменён несколькими другими (что бывает достаточно редко), то мы просто на его месте поставим микросервис-прокси, который будет реализовывать старое API и преобразовывать его в новое — это в любом случае необходимо для сохранения совместимости API.

Что делать с наборами удобных браузерных Developer Tools? url с параметрам в виде списка (не надо заходить смотреть тело), фильтры
А уже есть «XML over JSON»? Чтобы были все фичи XML и написано было по-json'овски?
(уверен, что выглядеть будет в миллион раз хуже, чем просто xml)
Вы не поняли. В XML есть типизация, из коробки, в JSON такого нет. Тут еще и сделали аналог XML-RPC. Я уверен, что уже и для типизации что-то придумали, что-то настолько страшное, что лучше пользоваться XML и не придумывать очередной xxx-RPC.
Я уверен, что уже и для типизации что-то придумали

Ну а как же! JSON Schema :)
Хотя это калька с XML Schema и не совсем типизация, но и типы и структуру валидировать может.
Впрочем, документировать такой простой API можно хоть в markdown-файле.

Извините, но это лукавство какое-то. Если у нас есть 200 сущностей, над которыми можно осуществлять 5 операций, то, условно, фронтендщику надо дать документацию на 1к возможных вариантов (эндпоинт + что в него слать и что вернётся в ответ на get/put/post + параметры). Если функционал остаётся тот же, но вы используете jsonrpc, то только переносите часть информации одействии из урла+метода в тело, но эта тысяча возможных вариантов взаимодействия никуда не денется и документировать надо ровно столько же.
Если у нас есть 200 сущностей, над которыми можно осуществлять 5 операций

В этом случае подойдет именно REST

По мне выгоды от JSON-RPC, в общем случае, нет.
URI это просто параметр в теле HTTP запроса, вы его из заголовка 'GET URI', перенесли в body в чем профит?)
А разделить обработку входных данные на основании пути достаточно удобно и стандартно:


/path1/val1
… обработчик 1…
/path2/val2
… обработчик 2 ..


Вам же прижется писать тот же routing только своими руками.
И не везде это применимо. Если у вас "сайт" с которым нужно взаимодействовать через браузер, очевидно будет выбор решения через url.

> вы его из заголовка 'GET URI', перенесли в body в чем профит?)

Забыли про HTTP-метод. В JSON-RPC вы не ограничены менее чем десятком методов со стандартной семантикой.

> Если у вас «сайт» с которым нужно взаимодействовать через браузер, очевидно будет выбор решения через url.

Только для загрузки «загрузчика» типа public/index.html, а реальные данные могут грузиться любым доступным способом.
Что-то не вижу преимуществ у JSON-RPC, в сравнении с GraphQL, а проблемы все те же. При этом у GraphQL есть ряд преимуществ: вложенные запросы, типизация, версионирование, возможность подписки на обновления данных (стандартизированная), разделение запросов и мутаций на уровне схемы, документация на уровне схемы, ну и куча тулинга на любой вкус и язык, хотя и на JSON-RPC его не мало, но банальной интеграции с девтулзами, чтобы нормально видеть запросы, я так и не нашел.
В общем, не понимаю зачем выбирать JSON-RPC вместо GraphQL сегодня
Разделение запросов и мутаций для вас, видимо, плюс, а для кого-то минус. Главное преимущество RPC (любого) — это вызов процедур.
Не представляю в каком случае может быть минусом возможность сразу определить идемпотентность запроса. В RPC получение и модификацию данных также разделяют, только с помощью префиксов (fetch/get и т.д.), документации и других нестандартизированных договоренностей, которые меняются от реализации к реализации.

Я немного экспериментировал с добавлением в имена RPC-методов префиксов Safe и Idemp, чтобы на клиенте было проще реализовать единообразную обработку некоторых типов ошибок. Клиент писал не я, но, по-моему, принципиальной разницы это не дало. Прикол идемпотентных REST-запросов не столько в том, что их можно повторить, сколько в том, что их повторят автоматически без нашего участия (браузер) — а в случае RPC прикол в том, что без нашего участия почти ничего не делается, больше контроль над происходящим но и больше ручной работы, в т.ч. обработки сетевых ошибок. А раз код обработки этих ошибок всё-равно надо писать, ручная автоматизация повтора части запросов в случае достаточно редких сетевых ошибок оказывается не настолько полезной, чтобы на это сильно заморачиваться.

Согласен. REST бесит тем, что данные размазаны по всему запросу. Параметры пути, query args, JSON-тело, заголовки… а в случае с RPC все в одном месте. Стандарта нет, всегда срач. Никогда не знаешь, какой метод выбрать — PUT, PATCH, etc.


Еще удобно, что тело RPC-запроса легко отдать в очередь типа Кафки и процессить воркерами.

Никогда не знаешь, какой метод выбрать — PUT, PATCH
PUT — замена ресурса или создание в случае отсутствия (RFC 7231)
PATCH — изменение ресурса в случае наличия (RFC 5789)

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

Разве REST предполагает такой сценарий использования? Batching же никак не стандартизирован, насколько мне известно. Есть только вот такой draft http-way батчинга, с ним никаких проблем не должно возникнуть

При чем тут батчинг? У вас может быть такая бизнес-операция, которая меняет несколько разных сущностей.


Кстати, вы напомнили про еще одну беду REST. Что якобы сущности изменяются отдельно друг от друга, а это не так.

У вас может быть такая бизнес-операция, которая меняет несколько разных сущностей
В REST такая операция дожна быть отдельным ресурсом, поскольку за один запрос вы можете производить какое-либо действие только над одним ресурсом/коллекцией ресурсов

Ага, вместо "POST someComplexAction" с одним ответом делаем "GET/POST/UPDATE/DELETE someComplexActionRequest" с разными. В чем профит?

О, у нас получился RPC over REST.
>_<
Мдя… я не понял почему статья противопоставляет REST и RPC. Т.е. на безе REST не может быть RPC? Скажу как есть — может. И часто реализуется, как внутренние типизированные интерфейсы. Например на базе REST есть шлюз /prc/ который реализует собственно rpc путем нескольких REST методов.

Суть самого определения RPC это удаленный вызов процедур. Как не банально это звучит. Он не заменяет REST который — архитектурный стиль взаимодействия компонентов распределённого приложения в сети. Нужно ли говорить, что вызов REST не подразумевает вызов конкретной процедуры?

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

RPC можно прекрасно поднять на WEBScoket канале. И очень эффективно реализовать его потенциал для online коммуникации с сервером. REST тут в принципе не может быть. Это нонсенс.

RPC очень полезная штука, но я бы сказал в совокупности с манифестами. Это позволяет держать консистентным канал связи клиента и сервера, а также, решает проблему документирования и развития интерфейса. Для бизнес-приложений с множественными интеграциями server-server по прямым каналам связи очень полезная штука.

На REST следует останавливаться тогда, когда вы делаете именно — взаимодействие компонентов распределённого приложения в сети. Т.к. REST предполагает влияние вашей бизнес-логики на транспортный слой. И это должен быть осмысленный выбор в его пользу.

Возьмите тогда уж SOAP, там будет вообще всё: от WSDL до отсутствия нужды реализовывать протокол на клиенте. А там, глядишь, и CORBA захочется.
REST тем хорош, что как раз никакой схемы нет — там поменял, сям поменял, бац-бац и в продакшн.
Доверите такой системе свою зарплату считать?
xml слишком многословен и затратен при кодировании/декодировании
UFO just landed and posted this here
а вы про какой софт?
для клиентского это не критично, а для серверного вполне может быть узким местом.
Например, для API Gateway это критично. Это критично для сервиса, который оперирует данными в памяти.
UFO just landed and posted this here
1. разговор шел json vs xml без относительно протокола, по которому они передаются
2. как будто HTTP парсить не надо
UFO just landed and posted this here
В моем текущем проекте софт при некоторых условиях должен делать полную синхронизацию. Это значит что надо вытащить через API все данные по клинике и распарсить их. Затраты времени на парсинг большого объема данных в соотношении ко времени передачи — 2 к 1.
При обычной работе объем данных на один запрос мал и соотношение обратное 1 к 2 (парсится быстрее чем передается). А вот полная синхронизация может занимать несколько минут — суммарно на сотню запросов может уйти 1-2 минуты на передачу + 2-4 минуты на парсинг.
Выход конечно найден, но факт есть факт. Когда надо вытянуть всю информацию по нескольким сотням клиентов за раз — текстовый формат передачи не очень подходит.
UFO just landed and posted this here
REST API должно вроде как реализовывать HATEOAS, и не может быть заменено в этой части на RPC-протоколы.

HATEOAS в теории позволяет машинам читать REST API и безо всяких там документаций. В реальности REST API, поддерживающих HATEOAS, почти нет от слова совсем — их считанные единицы. А всё то, что мы называем REST API просто случайный набор эндпоинтов, которые мы именуем, как душе угодно, и описываем это всё в лучшем случае только в документации.

Если это учитывать, то становится понятно, что нет никакой разницы, где именно мы напишем ключевые слова — в урле или в json-пакете.

На мой взгляд, получить HTTP 200 с ошибкой внутри — правильно, т.к. HTTP в данном случае выступает как транспортный протокол. Но это уже дело вкуса, т.к. ложки-то не существует.
REST API должно вроде как реализовывать HATEOAS, и не может быть заменено в этой части на RPC-протоколы.


Почему?
Ведь тут дело всего лишь в ответе.
Любой способ можно использовать, он все так же может возвращать в ответе дополнительную информацию «что делать еще».
Но это будет уже не REST, а просто «какое-то API на HTTP», вот в чем соль. В концепции REST обязательно возвращать все возможные действия и ссылки на ресурсы в каждом ответе :D И в этом как раз вся суть REST, и без этого это действительно «просто API over HTTP», что мы и наблюдаем в действительности. Без этой приписки к ответу требуется писать доку (полюбому же писать) и тогда у такого API нет объективных преимуществ перед RPC — только концептуальные и субъективные (вкусовщина).

А так как один хрен доку писать и это какое-то безумие в каждый ответ вкладывать полный список возможных действий и урлов — то это никто и не делает. В итоге мы имеем что имеем — зоопарк реализаций, подходов и даже философий.

И это хорошо. Лучше делать как удобно конкретному проекту, чем корчиться в угоду некой философии.
Несколько вопросов:
1. Eсть ли какие-то консольные утилиты для «jsonrpc over tcp» на подобии curl?
2. Есть ли «keep-alive» режим? Понятно, что от стандарта это не зависит, но реализуют ли это авторы библиотек?
3. Как передавать бинарные данные? Конвертация в base64 ведет к распуханию и падению производительности, какие есть «бест-практис» в этом плане?
Eсть ли какие-то консольные утилиты для «jsonrpc over tcp» на подобии curl?

А чем nc (netcat) не устраивает?

  1. Ответили выше — echo '{"jsonrpc":"2.0",…}' | nc host port. Либо за 15 минут пишется своя тулза, если нужен более удобный UI.
  2. KeepAlive для самого RPC не требуется, он stateless. Но если это нужно на уровне приложения (напр. это чат и нужно знать что юзер ещё онлайн) то делается отдельный RPC-метод "ping", который клиент должен периодически вызывать. Если это нужно на уровне транспорта, обычно в случае TCP, то на сокете включается штатный keep-alive средствами OS (слой бизнес-логики/RPC об этом не знает).
  3. В JSON нет поддержки бинарных данных, поэтому либо base64 (обычно для однозначности в доке API нужно ещё указать какой именно вариант base64 и нужно ли выравнивание), либо, для передачи больших объёмов данных (вроде видео), используется HTTP или REST, а на уровне RPC бегают только соответствующие url-ки.
2. Речь не про stateless, а про скорость, с keep-alive может работать на порядок быстрее т.к. не нужно создавать коннект на каждый запрос.
3. Как вариант, для «over http» можно бинарь вкладывать вторым в multipart, тогда и json и бинарные данные полетят в одном запросе.

Какие коннекты, протокол-то транспорто-независимый и про коннекты ничего не знает. Вы можете открыть одно TCP-соединение, после чего отправлять по нему RPC-запросы и ответы на них хоть год. Более того, обе стороны могут одновременно быть и клиентом и сервером, и отправлять запросы друг-другу всё по тому же одному соединению. Всё, что для этого необходимо (мультиплексирование запросов в одном соединении) в протоколе уже есть — это поле "id".


multipart это плохой вариант, потому что на уровне RPC-сервиса доступа к транспортному уровню нет, так что добраться до второй части multipart не получится.

Какие коннекты, протокол-то транспорто-независимый и про коннекты ничего не знает.
Я про это указал в вопросе.
Вы можете открыть одно TCP-соединение, после чего отправлять по нему RPC-запросы и ответы на них хоть год
Если библиотеки не поддерживают потоковую обработку json, т.е. если пришел не весь кусок json или несколько json слипились в один пакет и т.п., тогда это нельзя сделать с текущими либами и нужно писать свой велосипед.

multipart это плохой вариант, потому что на уровне RPC-сервиса доступа к транспортному уровню нет
Вы же говорите, что «протокол-то транспорто-независимый»
Если библиотеки не поддерживают потоковую обработку json

Какие именно библиотеки? Библиотеки для JSON RPC 2.0 — да, не поддерживают (точнее, если их написали корректно — транспорто-независимо — то не поддерживают). Библиотеки для транспорта — ну так транспорт для JSON RPC 2.0 не стандартизирован (не считая HTTP), так что каждый сам придумывает себе транспорт и пишет под него библиотеку, если есть что писать.


В случае TCP обычно делают одно из двух: либо сериализуют JSON "в одну строку", чтобы внутри гарантированно не было переводов строк, и при передаче добавляют перевод строки после каждого запроса/ответа (а "библиотека для транспорта" читает построчно и "библиотеке для JSON RPC 2.0" отдаёт отдельные строки-запросы/ответы), либо сериализуют как попало и используют потоковый парсер JSON в "библиотеке для транспорта".


Что Вам неясно насчёт multipart я не понял.

2. я так понимаю вопрос про jsonrpc over http
со стороны сервра: сервис обычно спрятан за каким-нибудь nginx, который умеет HTTP 1.1, если напрямую, например, в локалке, зависит от ЯП или фреймворка, но обычно проблем с этим нет
со стороны клиента: строится так же на базе уже готового http клиента, который умеет HTTP 1.1
Насчет кеширования и прочего middleware.

Как вариант, в случае использования http протокола, можно просто использовать часть url вместо ключа method в json'e запроса. К примеру `http://site.com/rpc/:method_name`. А в приложении первым делом парсить этот юрл, превращать запрос в стандартный json-rpc формат и передавать дальше в бизнес логику.

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

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

Эта спецификация предложена ещё в 2009 году.

А XML-RPC в 1998, и JSON-RPC таки стал ещё одним стандартом :)
Эта спецификация предложена ещё в 2009 году.
А можно еще какой-нибудь ГОСТ 70-х готов откопать. Уверен, что если поискать, то можно что-нибудь найти. Сей стандарт можно уместить в две строки:
— Запрос делай так…
— Ответ делай так…
И сразу возникает несколько вопросов:
— А вдруг мы хотим передавать метод в URL.
— Нам не нужно два свойства для ошибки, а достаточно одного.
(еще очень много вопросов)

На эти случаи будем другие стандарты писать?
А можно еще какой-нибудь ГОСТ 70-х готов откопать.

Можно. Если стандарт хорош под ваши задачи, почему бы и не придержаться его?


И сразу возникает несколько вопросов:
— А вдруг мы хотим передавать метод в URL.
— Нам не нужно два свойства для ошибки, а достаточно одного.
(еще очень много вопросов)

На эти случаи будем другие стандарты писать?

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

Вот только миллион однотипных стандартов == их отсутствию.
Нет. Как минимум, стандарты документированы и худо-бедно есть сообщество вокруг них. Даже локальный стандарт проекта/компании лучше чем отсутствие какого-либо стандарта.
Ещё такой вопрос, допустим вы запустили json-rpc сервис на нескольких серверах и сбалансировали через nginx (или через что-нибудь другое), и нужно вызвать одну процедуру но на всех запущенных серверах (типа pubsub с ответом).
Как бы вы это решили? Дополнительно внедрять MQ-сервис (rabbitmq/...) для этой процедуры или как?
(И ещё вариант если нет прямого доступа к серверам, только через балансер).

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

Все каменты не асилил, возможно повторяюсь или просто работаю капитаном очевидность.
1.
Если взять аббревиатуру REST и расшифровать ее, то получим Representational State Transfer — «передача состояния представления» — тоесть есть некоторые ресурсы(чего в определении нет, но про это написано много) и протокол направлен на то, чтобы просто и эффективно передавать состояние этих ресурсов между клиентом и сервером. То есть надо всего лишь передавать состояние ресурсов и команды на их изменение. Это можно сравнить с CRUD операциями в СУБД. К слову, это помогает автоматизировать типичные операции в например админках.
2.
С другой стороны, если расшифровать RPC, то получим Remote Procedure Call, тоесть вызов некоторой «подпрограммы» на удаленном сервере, которая делает какую то сложную магию. Это можно сравнить с бизнес операциями над СУБД, состоящих из нескольких(десятков) CRUD над разными таблицами и еще много чего. И да, батчи тут очень даже к месту.
3.
Поэтому, с моей точки зрения не надо натягивать сову на глобус и искать серебрянную пулю. Я думаю что тезис «все в вебе есть ресурс и его состояния» рано или поздно приведет к проблемам, равно как и тезис «давайте делать по RPC на выборку каждой сущности по ID» в проектах больше HelloWorld.
Representational State Transfer — «передача состояния представления» — тоесть есть некоторые ресурсы(чего в определении нет, но про это написано много)
определение ресурса есть в https://tools.ietf.org/html/rfc3986
Да, прям оттуда:
This specification does not limit the scope of what might be a resource; rather, the term «resource» is used in a general sense for whatever might be identified by a URI.

Сферические кони в вакууме — ни больше ни меньше, что в принципе логично для столь обобщенной спецификации.

История с REST API чем то похожа на другие классные штуки, как например чисто функциональное программирование или нормализация данных. Архитектура, построенная на простых принципах, но непростая в понимании и имплементации. Это порождает перманентное желание срезать углы при разработке. Желание понятное, главное правильно оценивать pros & cons.

Как в JSON-RPC реализовать валидацию форм — удобную и без велосипедов? В REST можно прислать общий код «Форма не верна» (422, например), а в теле ответа — массив/объект с информацией обо всех полях, заполненных ошибочно, после чего клиент легко и просто отображает ошибки на соответствующих полях.

Какая best practise для этого в JSON-RPC? Присылать по одной ошибке для каждого неверно заполненного поля, пока все они не окажутся заполненными правильно? Или изобрести свой формат поля data для подобного случая? Есть ли примеры готовой реализации?
А чем не устраивает запись того же «массива/объекта с информацией обо всех полях, заполненных ошибочно» в поле data?
Вполне устраивает. Просто для REST у меня была готовая реализация от фреймворка, где в случае ошибок валидации формы сериализатор выставлял в ответе нужный HTTP-код (422) и формировал нужную структуру массива ошибок полей формы. А в случае JSON-RPC пришлось писать подобный сериализатор самому.
В REST можно прислать общий код «Форма не верна» (422, например), а в теле ответа — массив/объект с информацией обо всех полях, заполненных ошибочно, после чего клиент легко и просто отображает ошибки на соответствующих полях.

например так:


{
  "jsonrpc": "2.0",
  "error": {
    "code": -32602,
    "message": "Invalid params",
    "data": [
      {
        "field": "login",
        "message": "Логин уже занят"
      },
      {
        "field": "name",
        "message": "Имя может состоять только из буквенных символов"
      }
    ]
  },
  "id": "1"
}
Да, ровно так и сделано: InvalidParams (-32602) для индикации ошибок во входных данных (поля формы в данном случае).
Еще один подводный камень, с которым вы, скорее всего, столкнетесь при работе с JSON RPC на стороне клиента — довольно скудный набор клиентских библиотек. Если же вы используете JSON-RPC over HTTP и хотите применять стандартную клиентскую библиотеку для сетевых запросов, вы столкнетесь с необходимостью переопределить то, что считается ошибкой и что — успешным запросом: в HTTP-клиенте эти ситуации различаются обычно по HTTP-коду (2хх и 3хх — все ОК, 4хх и 5хх — ошибка), а в JSON-RPC код будет почти всегда равен 200, а для различения error/success придется проанализировать тело ответа: если в наличии поле result — то запрос завершился успехом, если поле error — ошибкой.
код будет почти всегда равен 200
Для ошибок можно отправлять 400, завист от реализации
Некоторые клиентские библиотеки считают любой HTTP ответ успешным, их нужно дополнительно конфигурировать или вообще оборачивать, чтобы некоторые HTTP-коды бросали исключение, реджектили промис и т. п. Собственно, если это HTTP-библиотека, то это правильный подход.
Уф, я думал я один такой. :) REST вообще ни разу не использовал. Для сложных сервисов SOAP намного лучше, для остальных «колхоз» из servlet+JSON/XML.
Не один, но мы про json-rpc over whatever
UFO just landed and posted this here
Прекрасная статья, смысла в REST нету, если нету масштабирования

Смысл в REST (точнее в RESTish HTTP API) есть и без масштабирования, если хочешь использовать имеющуюся инфраструктуру. То же HTTP-кэширование.

Всё так. От себя добавлю, что документировать JSON-RPC всё также удобно с помощью JSON Schema. А если пишите на ноде, то перед вызовом метода можете делать схемную валидацию с помощью, например, www.npmjs.com/package/ajv. Как минимум меньше проверок валидности params делать в коде, хотя в ряде случаев их не избежать, декларативной схемой не покрыть всю валидацию.

Ну и наконец, с JSON-RPC чуть меньше удобств по просмотру лога запросов в девтулзе, и я искал расширение, но не нашёл.
Поэтому набросал черновой вариант: habr.com/ru/post/467767
Присоединяйтесь к разработке, там ещё как минимум надо фильтрацию и пакетные запросы.
Only those users with full accounts are able to leave comments. Log in, please.