Pull to refresh

Comments 128

Огромное спасибо!!! Всё по делу и всё понятно, без лишних обдумываний! +100500!
Давно прочитал эту книжку, очень полезная (спасибо одному польз. хабра за рекомендацию ее =) ), и кстати он Brian Mulloy, а не Malloy. :)
А Вы не станете ещё одним пользователем хабра, который скажет, что это за книга и посоветует её прочитать? :)
Мне было бы полезно
Статьи перечитывать тоже полезно :)
Книга «Web API Design. Crafting Interfaces that Developers Love».
Извините
Кое-что новое для себя узнал, благодарю за статью.
Существительные — это хорошо, а глаголы — плохо

/getAllDogs
/getAllLeashedDogs
/getDog
/newDog
/saveDog

Сам до сих пор грешу подобным, когда админскую часть пишу =/
не знаю, далек от проектирования Web API, но вариант с разными запросами кажется мне гораздо менее понятным, чем такой список функций, или я неправ?
Это же ООП получается — есть интерфейс «сущность», с четырьмя методами(POST, GET, PUT, DELETE) и дальше вы просто приводите список имплементаций интерфейса.(dog, cat, carrot)
А кейс с newDog/deleteCat похож на c-like API
А есть метод «SORT»? А есть метод «CHECK»? Мне всегда было непонятно, почему все считают, что ограниченного набора в 4 метода будет обязательно достаточно. Тем более, без хаков они всё-равно не используется. Просто использование ради использования, имхо.
SORT — это что, отсортировать записи на сервере?

CHECK — HEAD? :)
Ну да. Вот у вас есть форум типа пхпбб, там можно разделы сортировать между собой.
А, а API для этой операции требуется на случай если кто-то захочет управлять интерфейсом форума из другого приложения, я понял.
Сортировка контактов в списке IM
Сортировка покупок в списке планов на будущее
И так далее.
Если я правильно понял — то достаточно предусмотреть параметр ?sort=true|false, и список будет отдаваться отсортированным.
P.S. А, ниже уже отписались по этому поводу.
sort — это один из параметров метода GET который возвращает список сущностей.

м?
Операция чтения меняет данные

Между прочим, запросто. Операция «чтение темы» повышает в базе «счётчик прочитавших тему».
Может потребоваться произвести сортировку данных на сервере.
UFO just landed and posted this here
Ну простите, ну аргументы в стиле 2007 года: «у всех IE6, он не поддерживает эти ваши новомодные ..., их нельзя использовать, всё делаем на флеше». Где бы мы сейчас были с таким подходом?

Нужно фиксить кривые клиенты и прокси, вместо того, чтобы портить API.

Что касается IE8, то начиная с висты он не актуален (есть обновления как минимум до IE9), а XP уже найти всё труднее и труднее… а уж если и найти, то у какого-нибудь гика, у которого никак не ie.
Спасибо большое. Хорошо получилось. Просто, понятно и перевод удачный.
Когда то пытался прочитать эту книгу, да вот не сложилось. А тут прям конспект. Шикарно.
скорее всего, это будет уже отдельно :)
Статья класс! Можно подробней по поводу авторизации через API?
Почитайте документацию того же Твиттера. На первый взгляд она немного запутана, но дает хорошее представление об OAuth и подписи HTTP-запросов.
поправка, она дает хорошее представление об OAuth в реализации Twitter
Я оставил ссылку на странцу со скачкой книги в меню поста. Apigee разрешают скачивать её только после регистрации, так что не будем им ломать планы прямыми ссылками на файл :)
К сожалению, свой пост уже исправить не могу, чтобы убрать прямую ссылку. Но я её некоторое время назад тупо нагуглил: по запросу «web api design» первая же ссылка на оригинал в PDF. Google ничего не знает о том, что её можно качать только после регистрации.
В дополнение к статье хотелось бы еще добавить, что сегодня не стоит забывать про метод PATCH для частичного изменения ресурса. Вот пример его использования в API гитхаба.
К слову, у гитхаба очень аккуратное и грамотное апи, рекомендую.
Это прекрасно, тысяча чертей, огромное спасибо! Как заставить горе-архитекторов это читать?
Спасибо, очень годные советы. Единственно, мне кажется, для редактирования более подходит метод PATCH
Не все веб-сервера его поддерживают, а из JavaScript без эмуляции его не вызвать.
Если ваш клиент может посылать любые запросы и вы можете котролировать бекенд, то это достаточно неплохое решение.
А мы еще юзаем префиксы в базовых урлах, которые:
* определяют локаль в которой нужно выдавать данные
* определяют клиента (мобильное устр-во, телевизор)
пример /tab_app/ru_ru/films/get-all?offset=0&limit=50
А почему от типа клиента зависит результат запроса к api?
Разные тексты, картинки, ссылки на ресурсы… адаптивно же должно быть.
А что мешает перенести «служебную» информацию в хедеры? Язык — «Accept-Language», токен — «X-Access-Token», девайс — «User-Agent» или «X-User-Agent-Class», и не надо тогда пути несемантическим мусором забивать. «tab_app», «ru_ru» — это модификаторы только небольшой части содержимого, а не всего ресурса, и ладно бы они в параметрах передавались, так они ещё и в корне пути стоят. /me цокает языком. :)
ооо… на то много причин :) и по опыту не надёжны эти заголовки… прокси всякие, в логах не хранятся, ссылку не передашь…
Если вы цокаете языком то пока просто не дошли до граблей :)

PS
подумайте почему на сайтах языки в урлах.
За 4 года с проксями и хедерами, тьфу-тьфу-тьфу, никаких проблем, в логах всё замечательно хранится, а передавать ссылку на API… ну… я даже не знаю что тут сказать. :)
ну ничего, раз не знаете :) я ж не настаиваю
Просто не встречал протоколов которые подмешивают X-хэдеры в свою солянку.
Вы так делали где нибудь?
Да, локаль и платформа девайса — в хедерах передаются, всё прекрасно работает. В некоторых случаях и single_access_token'ы, чтобы в логах не светить лишний раз. И so far это всё отлично работает. А что, с таким подходом есть серьезные подводные камни? Расскажите, пожалуйста, будет интересно узнать, чего стоит опасаться.
UFO just landed and posted this here
очень хорошая подборка советов.

Однако на практике часто красота жертвуется в пользу оптимизации запросов.
Стандартный кейс:

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

Как уложить в архитектуру такие запросы сразу нескольких объектов?

Каких именно нескольких объектов? Можно, например, вместе с инфо о товаре возвращать последние 10 комментариев и список фото для галереи товара, вместо того чтобы отдельно ходить за комментариями и отдельно за фотками.
да, я про такие случаи, как ваш пример с комментариями.
Это уже немного хак получается, что при получении объекта товара передваются еще зачем-то комментарии,
которые не всегда нужны в другом случае использования API.
Значит нужен доп. параметр (когда их передавать, когда нет), потом параметр лимит на число комментариев и т.д.

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


Хак или нет, но это довольно широко распространенная практика. В любом случае, когда вам по API надо вернуть объект, вы же не передаете голый набор object.attributes клиенту? Вы что-то излишнее убираете, что-то меняете, что-то добавляете. REST-сущность — это не обязательно строго информация о одной сущности и больше ничего — это набор рекомендаций, который предполагает что когда вы запросите сущность по rest — вы получите всю необходимую вам информацию о ней, включая так же доп инфо по усмотрению провайдера API.
На счет доп параметра, когда передавать, когда нет — это решается на этапе проектирования API. Комментарии либо нужны, либо нет. Если встает вопрос «иногда выводить, иногда нет» — значит проект был хреновый. Лимит не нужен — на етапе согласования решаем что нужно выводить 10 — выводим 10. Если кому-то 10 много — показывает 5, остальные игнорирует. Если кому-то 15 — делает доп.запрос. Если кому-то они не нужны — игнорирует комментарии.
Такие дела.
Тоже интересуюсь данным вопросом. Пока пришел к выводу отдавать с запросом дефолтное (определяемое константой) количество зависимых сущностей, а потом, догребать их другим урлом если надо:
GET /articles/1 — вернет {title:'bla', body:«bbla», comments: [10 комментариев]}
GET /articles/1/comments?skip=10&limit=100
«Преждевременная оптимизация зло»
До построения чистого API вам сложно будет что-либо сказать как ваши методы исспользуются.
После построения API, можна исспользовать например SPDY и мультиплексирование вызовов.
Ни и последний совет о псевдонимах.
А вообще это архитектурный вопрос, на API примтивного уровня строим API более высокого порядка, если есть потребность отдает более высокий уровень клиенту, только очень акуратно, чтобы 3 таких клиента «тяжолыми» на выборку всей базы не положили все приложение, а чем сложнее API, тем сложнее такие вещи контролировать.
Дополнительный слой с мультиплексированием де мультиплексированием в 100 строчек кода сохранит чистое и светлое API и клиентский код сделает чище
Где можно почитать поподробне?
Дополнить перевод одной фишечкой, которую все же стоит предусматривать при создании API стоит сразу закладывать, что будет версионность. В домене api.v1.....com или же ....com/api/v1/…
И про это, между прочим, в переводе написано :)
Мы реализовывали версионность с помощью заголовков Accept и Content-Type, вида «application/appname-v1+json». Это дает бОльшую гибкость API.

Хорошее обсуждение этой темы: Best practices for API versioning
UFO just landed and posted this here
Стоит ли так делать? RFC 4627 пишет, что не стоит вроде. Вот обсуждение на SO
Я верю, что он легальный. Я даже верю в то, что браузеры не всегда следуют рекомендациям rfc. Я просто хотел обратить ваше внимание на эти рекомендации.
Вместо глаголов — HTTP

Мы только что описали собак с помощью двух базовых URL адресов с существительными. Теперь нам нужно работать с созданными сущностями. Чаще всего требуются операции чтения, создания, редактирования и удаления (CRUD — Create — Read — Update — Delete). Для этого нам прекрасно подойдут HTTP-методы GET, POST, PUT и DELETE.
POST /dogs — создать новую собаку
GET /dogs — получить список собак
PUT /dogs — редактирование всех собак сразу
DELETE /dogs — удаление всех собак

POST /dogs/12345 — вернуть ошибку (собака 12345 уже создана)
GET /dogs/12345 — показать информацию о собаке
PUT /dogs/12345 — редактировать собаку 12345
DELETE /dogs/12345 — удалить

Базовые URL выглядят просто, глаголы не используются, все интуитивно и понятно. Красота!

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

GET /dogs — получить список собак
POST /dogs/add — создать новую собаку
POST /dogs/update — редактирование всех собак сразу
POST /dogs/delete — удаление всех собак

GET /dogs/12345 — показать информацию о собаке
POST /dogs/12345/update — редактировать собаку 12345
POST /dogs/12345/delete — удалить

Это позволит добавлять новые (не входящие в набор CRUD) методы (например, "/dogs/12345/archive") не отходя от схемы именования и (опять же, на мой взгляд) это куда более нагляднее и понятнее, чем использование методов PUT и DELETE (малораспространённых, требующих дополнительных разъяснений и дополнительных технических возможностей от клиента).
Да уж, или попадется какой-нибудь клиент, который ничего кроме GET и POST не умеет. Вариант с ?method=DELETE выглядит корявее, чем явное указание действия.
Извините, я немного не в курсе. А бывают такие клиенты?
В спецификации HTML 4.01 закреплено, что тег «form» поддерживает только GET и POST в качестве возможных значений аттрибута «method».
Даже в html5. Но я спрашивал про самих клиентов — может какие-то распространенные программы вообще не позволяют использовать методы, отличные от get и post?
Да, конечно среди распространённых браузеров вроде бы нет урезания функционала по выбору метода.
Но клиентом может быть и не браузер. API и создаётся для того, чтобы доступ к данным не зависел от клиента.
Я не исключаю случаи, что в каких-то устройствах возможны только методы POST и GET.

Например, есть такие gps трекеры от компании GlobalSat, которые могут передавать данные о местоположении по http протоколу. Трафика получается больше, чем по чистому tcp, но, может, доступа до слушания tcp на сервере нет.
Я никогда не работал с такими трекерами, но чую, что там только посылается исключительно GET запрос.

UPD. Как-то странно получилось, что я сам ответил на свой вопрос в этой ветке.
Да, конечно среди распространённых браузеров вроде бы нет урезания функционала по выбору метода.

Ну вот сейчас я попробовал создать HTML форму с method="put" и отправил её через браузер google chrome — метод был заменён на «get».

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

Думаю, тут надо смотреть в сторону различный библиотек для языков программирования, которые могут использоваться для отправки HTTP запросов серверу (не думаю, что тут будут какие-либо серьёзные ограничения, т.к. от клиента требуется лишь сформировать соответствующую строку HTTP запроса).
метод был заменён на «get».

Интересное поведение. Да, я ж говорил, что в html (4 и 5) значение method у формы может быть только get|post. Тем не менее, браузер может отправить http запрос с другим методом, например при помощи XmlHttpRequest.
А интересно, для каких ещё способов отправки из браузера вообще может быть полезен restful api с json-ответами?
Ну это очень странное высказывание. POST запрос ничем принципиально от PUT или HEAD не отличается. Данные отправляются в одном и том же виде, в запросе меняются только слова POST, PUT, HEAD (или любой другой глагол).

Если речь идет о браузерах, которые не умеют отправлять ничего кроме GET и POST, то они умеют :)
И пример с form вообще не показателен. Мы же говорим о работе с API, а я не очень представляю зачем вообще в данном случае использовать form.

Браузерный клиент, работающий с API должен будет получать, обрабатывать и отправлять данные; в зависимости от ответов сервера создавать и показывать пользователю страничку (то есть страничка больше не должна генерится как встарину сервером). А это в любом случае что-нибудь вроде JavaScript с помощью которого отправить PUT-запрос не сложнее, чем GET или POST.
Отличается. POST и PUT предполагают непустое тело запроса и, возможно, непустое тело ответа. HEAD предполагает пустое тело запроса и пустое тело ответа. Так что меняются не только слова.

Согласно RFC2616, PUT /some/URI означает, что тело запроса должно стать тем, что потом можно будет получить запросом GET /some/URI, т.е. создать или заменить этот ресурс.
PUT — идемпотентный запрос, т.е. выполняя его два или более раз мы получаем то же самое, что получили после выполнения один раз.

POST /some/URI- передать некоторые данные ресурсу /some/URI. POST — не идемпотентный запрос.

ну, а HEAD — это вообще обрезанный GET. Данные (тело запроса) вообще никакие не отправляются и не принимаются (тела ответа нет), только метаданные (заголовки).
Я не про этого говорил, а про то, что если клиент умеет отправлять POST и GET запросы, то отправить запрос с любым другим глаголом, вероятно, тоже сумеет.
Постойте, а разве в http 1.1 метод не может быть произвольным? Согласно rfc2616 «All other methods are OPTIONAL»
Но для этого веб-серверы должны обрабатывать все произвольные методы как GET или POST запросы (чтобы запрос с подобным методом дошёл до скрипта). Думаю, что под данной фразой имелось ввиду то, что веб-серверы могут реализовывать собственные специфические методы.
Стоп, в RFC полная фраза выглядит так: «The methods GET and HEAD MUST be supported by all general-purpose servers. All other methods are OPTIONAL». Т.е., по видимому, имеется ввиду, что методы PUT, DELETE и т.п. веб-агенты могут и не поддерживать.
>>Попробуйте посмотреть на ваши вызовы глазами пользователя. Вы увидите, что примерно 80% вызовов принимают и отдают данные одинаковой структуры. Это значит, что вполне можно сделать псевдонимы для последовательностей вызовов.

А можно парочку примеров? А то мне в голову приходят опять таки, только уродства типа /getDogsAndFoods или /getInitData
Например, нужно получить запись и комментарии к ней и это типовая операция. Вместо двух запросов (Запись/666 и КомментарииКЗаписи/666) комментарии (или первая их порция) включаются в ответ на запрос Запись/666.
… если следовать тому, что написано в статье, будет либо

Записи/666
Записи/666/Комментарии

либо (один запрос)

Записи/666? включить-комментарии=1
Я, кстати, предпочитаю делать аналогично выводу ls -F:

/people/ — для списка
/people/123 — для конкретного

То есть, слеш в конце определяет, список это или нет. Это особенно удобно для под-ресурсов типа:

/account/profile — для профиля «текущего» юзера. Сразу ясно, что никакой это не список, а конкретный ресурс.
>Вместо глаголов — HTTP
… после чего вы наслаждаетесь какой ни будь прокси, проглотившей PUT и DELETE.
Не говоря про то, что автор просто не в курсе спецификаций http, где скорее уж PUT, а не POST должен выступать в роли «create», а для «update» скорее подходит PATCH.
Тем более POST как глагол означает не совсем create, а PUT не совсем update. Рассогласованные имена это хороший способ запутать всех, включая и автора статьи.

Я бы рекомендовал начинать с надежно работающего враппера вроде:
{
    "method": "update",
    "data": {
        ....
    }
}


Получаем в ответ

{
    "status": "success",
    "statusCode": "200",
    "message": "ok"
}


И только потом развлекаться с попыткой корректно использовать для передачи директив HTTP методы.

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

Также вы окажете одолжение разработчикам под ваше API, если в дополнение к протоколу опубликуете схемы передаваемых данных в виде JSON Schema, Avro и т.п. Будет проще и с документированием и с версиями.
Сплошная вкусовщина и противоречивые параграфы. То «использовать глаголы плохо», то «для действий используйте глаголы». Сразу бы написали, что «для сущностей используйте существительные, а для действий — глаголы», хотя это конечно очевидно.

Потом какой-то странный совет про CRUD over GET — это вообще за гранью добра и зла. Ладно бы ещё написали, что для получения данных использовать GET, а для изменения POST — этого достаточно для реализации чего угодно.

И вообще, CRUD — далеко не лучшее решение во многих случаях. Начиная с того, что Create и Update во многих случаях практически идентичные операции (разница лишь в том передаётся id записи или нет), заканчивая тем, что и действий с сущностями гораздо больше: Feed, чтобы покормить собаку, Walk, чтобы выглять и тд.

Коды ответов то рекомендуют использовать соответствующие HTTP коды, то говорят, что по умолчанию лучше выдавать всегда 200 o_0" И вообще много таких советов как усложнить себе жизнь: сделайте поддержку HTTP методов, а потом прикрутите их эмуляцию через ограниченный набор. Коли уж нужно втискиваться в рамки ограниченного набора — ну так и сделайте апи в этих рамках без всяких эмуляций. Незачем переусложнять.

Далее, префиксные параметры — позиционные с соответствующими косяками:
* Параметр всё-равно надо указывать даже если он не нужен. Например, мы хотим, чтобы язык выбирался автоматически — вместо /ru/ придётся писать что-нибудь типа /auto/. Либо допускать его не указание, но для этого нужны будут костыли типа «список возможных значений».
* Сложность добавления параметров. В серединку их добавить можно лишь со скрипом.
* Не всегда очевидно что означает тот или иной параметр. Имени-то у него нет. А если ещё и портянка из /auto/auto/ — вообще беда.

Опять же, псевдостатика хоть и популярна, но всё же менее понятна. Согласитесь, запись вида /ключ1/значение1/ключ2/значение2 (/owners/vasya123/dogs/bobby) — это какая-то хрень. Куда логичней иметь что-то типа /owner=vasya123/dog=bobby, а если чуть подправить пунктуацию, то получится всем известный формат ?owner=vasya123&dog=bobby

Но я бы предпочёл, что-то типа такого:

?dog;list для работы со списком собак
?dog=12345 для работы с отдельной собакой
?dog;list;owner=5678 для работы со списком собак отдельного человека
?dog;list;color=red;state=running;location=park для сложной фильтрации
?dog;list;api=13 просто указываем версию апи дополнительным параметром, если не указана — ну делаем редирект на «нормализованный урл» подставляя последнюю версию.
?owner;list;fields=name=address=dogs элегантный способ передать список значений, хотя и спорный, так как для поддержки такого формата сейчас нужно велосипедить.
?dog;list;offset=50;limit=50 пагинация
?convert;amount=100;from=EUR;to=USD конвертация 100 евро в доллары
?dog;list;format=json вместо формата по умолчанию используем json
?search=search+word глобальный поиск
?serch=search+word;type=owner=dog поиск по собакам и их хозяевам

Кстати, json далеко не лучший формат представления данных в апи. Потому что имеет косяки с расширяемостью. XML конечно тоже имеет проблемы, но сильно меньше. Зачастую изменение формата JSON приводит к необходимости увеличивать версию апи, потому что код работающий со старым форматом начинает падать на новом. В XML больше возможностей для расширения благодаря более абстрактным структурам (дерево из именованных узлов вместо массивов и хэшей в JSON). Есть ещё формат Tree, который ещё лучше чем XML в этом плане, правда библиотек под него почти что нет hyoo.ru/?article=%D0%A4%D0%BE%D1%80%D0%BC%D0%B0%D1%82+Tree;author=Nin+Jin

Для api можно вообще не заводить отдельный домент, например, на сайте hyoo.ru сервер всегда выдаёт довольно компактный api-xml, который в браузере преобразуется в страницу с помощью xslt.
Кстати, json далеко не лучший формат представления данных в апи. Потому что имеет косяки с расширяемостью. XML конечно тоже имеет проблемы, но сильно меньше. Зачастую изменение формата JSON приводит к необходимости увеличивать версию апи, потому что код работающий со старым форматом начинает падать на новом. В XML больше возможностей для расширения благодаря более абстрактным структурам (дерево из именованных узлов вместо массивов и хэшей в JSON).

О каких косяках с расширяемостью Вы говорите? JSON и XML оба предназначены для представления древовидных данных, просто у JSON синтаксис более компактный. И они оба позволяют создавать дерево из именованных узлов.
Здорово поддерживать несколько форматов ответа.
Google ?alt=json
Foursquare /venue.json
Digg ?type=json

Кстати, Digg позволяет установить формат ответа и через HTTP-заголовок Accept.


Нужно ли тут использовать костыли? В спецификации HTTP четко написано:
The Accept request-header field can be used to specify certain media types which are acceptable for the response.

Если какие-то прокси реализовуют протокол не правильно (не передают заголовки), то это по большей сути не ваша проблема. Иначе зачем вообще придумавать какие-то спецификации. А при использовании заголовков все просто и понятно.

ps: только недавно диплом защитил на эту тему, приятно видеть что на 90% решения выбранные в нем пересекаются со статьёй более опытных товарищей :)

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

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

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

Почему когда я пишу код который крашится или работает не правильно, тогда мне приходится (да я и рад) его переписывать. А когда производители устройств что-то партачят, мне приходится все их проколы программно закрывать?

Ввести бы сертификацию какую построже, прошел — получи значек красивый, не прошел — получи ужасный красный знак на пол коробки устройства о том что «сертификация не пройдена, подумайте еще раз перед тем как купить(установить) это». Глядишь и жизнь стала бы немного проще.
Отличная статья, ещё и весьма вовремя (для меня, разумеется ;) ).
Конечно, с некоторыми положениями можно подискутировать, но в целом все очень толково.

Единственное, что удивило — не рассматривается простая особенность GET и POST http-запросов в контексте создания API: первый идемпотентный, а второй — нет. Разумеется, свойство это обеспечивается не названием метода, а реализацией API, но в целом это хороший и правильный тон — использовать GET для получения данных, а POST — для их изменения.
Это как раз описано: GET для получения, POST для создания, DELETE для удаления…
Вы немного не поняли мою мысль. Можно DELETE, PATCH, PUT и т.д. использовать, я все это в статье видел. Мой коментарий о том, что если не использовать все это многообразие, то все же вполне разумно изменяющие данные запросы отсылать через POST, а все остальные (безопасные) операции — делать через GET.
Я перечитал RFC и теперь могу сформулировать, что же мне не понравилось в вашем комментарии :)

Во-первых, к определениям: идемпотентный != не изменяющий данные на сервере. Идемпотентный (по определению) — это такой, многократное выполнение которого эквивалентно однократному. Т.е. PUT, DELETE, GET, HEAD, OPTIONS — все идемпотентные, т.к. выполняя DELETE /что/нибудь четыре раза подряд мы получим ровно такой же результат, как если бы выполнили его один раз. (В математике: идемпотентный оператор равен себе самому возведённому в любую степень.)

Подход «всё изменяемое — через POST» не совсем корректен. POST предназначен для действительно «одноразовых» событий, которые могут повторяться, и каждое повторение — это новое событие, должно регистрироваться независимо. Например, добавление комментария к записи или отправка письма, каждый раз мы отправляем новый комментарий или новое письмо.
Например, PUT — создать или изменить ресурс. Выполнив его (с одним адресом) несколько раз, мы не создадим несколько ресурсов, мы один раз возможно создадим и несколько раз заменим ресурс по этому адресу. PUT /что/нибудь имеет смысл «создать объект, который потом можно будет получить запросом GET /что/нибудь», причём тело запроса PUT будет телом ответа GET. Это идемпотентная операция, ничего страшного, если её выполнить неоднократно, однако она изменяет состояние. DELETE дополняет PUT, отменяя его действие.

Смысл методов POST и PUT/DELETE, как видите, существенно отличается, хотя все они изменяют данные. Соответственно если мы вынуждены реализовывать такие операции, как удаление ресурсов, через POST/GET, то GET (в смысле идемпотентности) больше похож на DELETE, чем POST. Ну, и соответственно, уместнее.
Что за софистика =)

У HTTP-глаголов есть еще одна важная характеристика кроме идемпотентности — это безопасность.

Ознакомьтесь, пожалуйста, с ней.
Практическая истина такова, что все они небезопасные. GET тоже изменяет данные, хотя бы счётчики посещений. Так что нет у них безопасности.
Если честно я читаю, и вообще не понимаю о чем речь.
Метод — это просто строчка. Прокси, веб сервер и прочий middleware просто прокидывает эту строчку в приложение. А приложение уже наделяет эту стрчоку семантикой.
В этом смысле, не вижу проблем с методом SORT, сомневаюсь, что есть хоть какой-то вид софта, который заблокирует запрос, начинающийся с SORT только потому, что такого метода в RFC не описано
К слову, наличие или отсутствие тела на уровне парсера или прокси определяется по заголовкам Content-Length, Content-Type и Content-Disposition, а не по названию метода, так что даже для этих частей название не имеет значения.
Я не вижу проблем с методом SORT. Я вообще не вижу места для метода SORT в HTTP. Нет метода — нет проблем.

Поясняю.

HTTP — это протокол типа «запрос-ответ» без состояния. Ещё раз: протокол без состояния. Никаких методов, которые наделяли бы его состоянием, нет. Не должно быть. Если мы хотим вести речь о состоянии (сессии, куки), мы делаем это в надстройках, поверх протокола: используем заголовки, куки и тому подобное.

Ещё особенность: адреса, к которым мы делаем запросы, не перечислимы. Мы не можем средствами протокола получить список всех объектов (URI) на сервере. Это тоже делается средствами более высокого уровня, поверх HTTP. Значит, с точки зрения HTTP никакого порядка объектов не предполагается. Структура (иерархическая) предполагается, а порядок записей в рамках одной ступени иерархии — нет.

SORT — штука, которая заведомо предполагает наличие состояния, причём в виде некоторого порядка записей на сервере. Как она вообще может появиться в таком протоколе?
Да блин, вы написали столько текста, а лучше бы постарались внимательнее понять что я хотел сказать.
Ясен пень, что в HTTP нет состояния, никто не предлагает делать цепочку в стиле SORT->GET
Ну представьте что у вас есть каталог, в котором элементы физически упорядочены и SORT скажем меняет их порядок, не на уровне представления, а на уровне хранения. Ну скажем этот порядок имеет значение потому что эти элементы потом в таком порядке кем-то обрабатываются. (т.е. что-то вроде того что делает CLUSTER в постгресе)
Ну или забейте на SORT, если он вас так смущает. Представьте что вам нужен метод REPLACE для атомарной замены. Или TRANSFER для перевода ресурсов из счета А в счет Б. Помоему, отличный кандидат на звание «метода» в нашем API.

Про то что «не все клиенты поддерижвают произвольные методы». Это правда, никто этот факт не игнорирует, просто нравится строить API из предположения что большинство клиентов все-таки нормальные, а все остальные смогут заюзать какой нибудь workaround, скажем можно продублировать логику так, что все запосы можно сделать с помощью POST с глаголом, размещенным в теле запроса или заголовке.
Нет ничего упорядоченного на сервере. «REPLACE» и так есть, называется PATCH.

Что-то вроде TRANSFER могло бы появиться… правда, в логике HTTP оно называлось бы MOVE — переместить ресурс, как дополнение к GET — получить ресурс и т. д. Хотите сразу назову проблему с таким методом практически в любом клиенте?

А вот она: предполагается указание двух адресов. А формат строки запроса HTTP таков, что там нет места для второго адреса ресурса. МЕТОД РЕСУРС ПРОТОКОЛ/ВЕРСИЯ. Куда тут второй ресурс поместить? В заголовки? Ну, тогда, выходит, метаданные становятся обязательными (как заголовок Host в HTTP/1.1). Значит, появится некоторый стандартный заголовок (не-X-что-нибудь), который отдельные клиенты имеют право не знать и соответственно не желать отправить. Это только X-что-нибудь незнакомое разрешается отправлять.

Не, я не против таких методов. Но это уже будет не HTTP/1.1.

Вообще, если вам интересно, таких протоколов на базе HTTP есть дюжины. Есть IPP/1.1 (Internet Printing Protocol) — это супертип HTTP/1.1, там добавился метод, кажется, PRINT. В принципе, ничем больше от HTTP он не отличается. Есть протокол сервера трансляции Icecast, тот, который используется для подключения источников — тоже на базе HTTP/1.1. Тоже там добавилось пара методов вроде SOURCE и дальше адрес ресурса — это адрес точки монтирования источника. Даже SIP/1.0 похож на HTTP — главное отличие в том, что он поверх UDP и методы называются там REGISTER, INVITE и т. п., а коды состояния — ACK, DECLINE, BUSY и тому подобное.

Но это всё не соответствует RFC2616, то есть, не HTTP/1.1, и называется по-другому. И клиенты HTTP/1.1 (реализующие RFC2616) имеют право сознательно блокировать все эти некорректности.
Ну так зачем ограничивать себя довольно узким протоколом, если это не даёт никаких преимуществ? Вот хоть убейте — не понимаю. Зачем ограничивать себя методами GPPD, если они уже по умолчанию не покрывают всего, что нам может понадобится и всё-равно придётся вводить дополнительный слой?
Затем же, зачем существует стек OSI — чтобы упростить и развязать сущности.

Точно так же TCP отвязан от HTTP, и сегодня видно, как оказалось преимуществом: HTTP через TCP v6 оказался тем же самым протоколом, и веб-разработчикам вообще ни о чём беспокоиться не пришлось (кроме тех, кто считает, что IP-адреса нужны приложению — они сами сознательно привязались к IPv4 и им всё-таки пришлось поэтому беспокоиться).
И над HTTP тоже есть слои: например, HTML-страница. Она может и не быть над HTTP, а быть просто файлом на диске, от этого она не перестаёт быть HTML-страницей. А может быть и не страница, а что-то другое, картинка.

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

Пока я вижу только усложнение и завязывание. Вместо того, чтобы поступать просто — выбирают сложный путь, стараясь втиснуться в 4 стандартных метода, хотя это не имеет ни смысла, ни преимуществ. Одни недостатки.
Но это уже будет не HTTP/1.1

Еще как будет: www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.1.1
HTTP протокол не запрещает использовать произвольные имена методов.
Вы, мне кажеться, мешаете мух с котлетами. Протокол позволяет передать набор разных штук вроде метода, url, заголовков и тела в приложение.
Промежуточные узлы должны уметь поддерживать такую передачу.
То есть формальное описание HTTP — оно для разработчиков клиентского движка, разработчиков прокси и разработчиков веб-серверов.
А что вы туда положите — уже ваше дело. Не надо заявлять о проблеме того, что «метадананные становяться обязательными» — это не проблема уровня протокола. Клиентская библотека сможет закодировать новый заголовок? Прокси сможет его передать? Сервер сможет его прочитать и передать приложению? Если да, отлично, протокол справился.
HTTP не регулирует ограничения того, что клиент передает в запросах и что он расчитывает получить от сервера.
Возвращаясь к нашему примеру, это вовсе не HTTP-протокол обяжет вас передавать во втором заголовке путь назначения для MOVE. Вам это нужно будет сделать, что бы вас поняло приложение, которму вы шлете запрос. А протокольная часть она что, она закодирует ваш запрос без заголовка, и потом раскодирует ответ с ошибкой от приложения, которое не нашло заголовка и послало вам 400 Bad request.

Кстати, в SIP коды состояний такие же как и в HTTP — числовые. Только помимо 1xx-5xx(семантика которых совпадает с HTTP) есть еще 6xx.
ACK — это запрос(как PUT и GET), со спец. семантикой, а не код состояния. Но SIP это вобщем-то совсем не HTTP-протокол, просто он внешне похож.
Ну вы сначала приводите пример, что вроде бы протокол — это форматирование запросов и ответов, а как их понимать, дело приложения, а потом у вас SIP — совершенно другой, просто внешне похож.

А между тем по вашей же ссылке написано, что протокол HTTP определяет и семантику происходящего. Как прокси, так и сервер имеет право отказаться выполнять запрос, видя незнакомый метод, так как не знает его семантики (может, он вообще самому прокси предназначался, тогда его не надо было передавать). И ответить 501 Not Implemented.
Эмм, у SIP несколько другие свойства из-за того что он строится на UDP.
HTTP использует фичу TCP-соединения по поводу упорядоченности, и работает по принципу открыть соединение-отправить запрос-получить ответ по тому же TCP-соединению.
Это как раз свойства протоколов.

Когда мы говорим, что у нас есть метод MOVE, ожидающий заголовок Destination, мы создаем новый протокол, поверх HTTP.(а не меняем HTTP, и не создаем протокол похожий на HTTP) Вообще говоря описание API это и есть описание протокола.
Ну, о чём я и говорю — вот как раз таким образом получились протоколы IPP и Icecast. Надеюсь, спор о том, HTTP это будет или нет, можно закончить?
Очередное заблуждение.

Практическая истина такова, что часто люди делают веб-вервисы как попало, а потом еще осмеливаются называть свое творение RESTfull.

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

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

Но доверия этому счётчику при наличии такого кэширования всё равно нет. Сколько раз страница взята из кэша и соответственно посещение было, а счётчик не менялся? Сколько HEAD-запросов дошло до счётчика, и поэтому посещения ресурса фактически не было, а счётчик посчитался?

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

Подсчёт может делаться POSTом — он единственный не-идемпотентный метод с передачей данных.

Насчёт небезопасности GETа: да, я согласен, это плохо. Только мало смысла говорить о правильном использовании GET, если мы будем делать POSTом то, что нужно было делать DELETE, PUT или PATCH.

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

Другое дело, что причину подобного ограничения придумать сложно.
Спасибо за Вашу работу по изучению и обьяснению RFC (без сарказма).

Ваш текст, по хорошему, должен был присутствовать прямо в обсуждаемой статье )
Сколько читаю про идемпотентные запросы, так и не понимаю, как их реально можно применять в клиенте.
Вот допустим, хочу я добавить собачку владельцу. И тогда мне нужно сделать запрос
PUT /owner/123/dog/456
Вот откуда клиент может знать id нового, ещё не созданного, объекта? Получить от сервера предыдущим запросом? А если другой клиент опередит и создаст объект с таким id на миллисекунду раньше?
Если не передавать id и делать запрос PUT /owner/123/dog то сервер сам подставит новый id (да хоть AUTO_INCREMENT), и передаст его клиенту. Но это уже не идемпотентный запрос — десять таких запросов создадут десять объектов.
Выход вижу только в том, что делать нумерацию собачек не сквозную, а у каждого владельца с начала. Но такой подход тоже не всем подойдёт. Либо использовать PUT только для обновления данных, не создания (как, в принципе, в статье и предлагается). Но тогда разговоры об идемпотентности не стоят ни байта, о них написанного, ведь есть многие приложения, где не предполагается изменение данных (комменты, например).
А если другой клиент опередит и создаст объект с таким id на миллисекунду раньше?

А другой клиент своим запросом получит другой ID. Сделать так, чтобы разные клиенты получили разные ID — проблема сервера.

Вообще PUT применим, если вы реально знаете конкретное место, на котором будет жить объект. А для комментов предполагается использовать POST.
Ну вобщем предполагается что клиент генерит случайный ID из достаточно большого множества идентификаторов, что бы совпадения были исключены.
Ещё бы пару слов о возвращаемом JSON (best practices), нормализации (хотя это более общая тема, но постоянно вижу сервисы где запрос возвращает всё что можно), кешировании, ETAG, о том что возвращать в одном JSON поле то объект, то строку, то bool — плохо (постоянно такое вижу) и цены бы вашей и без того хорошей статье не было.
В любом случае спасибо!
Рекомендую заменить слово «паджинация» на «постраничность».
Я бы порекомендовал вам выучить слово «Пагинация». И больше не произносить вслух слово «постраничность».
Я бы порекомендовал вам забыть слово «Пагинация» — особенно при работе с заказчиками. Да и, вообще, с живыми людьми.
api.* -> developers.*
dev.* -> developers.*
developer.* -> developers.*
Навеяло Балмера.
Смело берите HTTP коды ответов и сопоставляйте с ответами вашего API.

Предусмотрите в своем API параметр suppress_response_codes и сделайте его равным true по умолчанию.

Не стыкуется. Видимо, по умолчанию false.
Sign up to leave a comment.

Articles