Pull to refresh

Comments 120

Лаконично и понятно, спасибо за статью. Довольно часто приходится сталкиваться с мифом/немифом о том, что вообще не стоит использовать операции отличные от GET/POST/PUT/DELETE, мол не по стандарту это, с отсылкой как раз к промежуточным узлам которые могу отказаться не в курсе, и программистам на принимающей стороне, которые тоже могут быть не готовы, а в некоторых случаях — и вовсе не иметь инструментов для обработки таких запросов. В итоге вся логика которая в статье выкидывается на PATCH, например, запихивается в хитрый PUT. И так далее. В принципе, вся статья как бы намекает на ваше отношение к этому, но тем не менее — выскажитесь пожалуйста )
В 2015 году промежуточных узлов, не умеющих PUT/PATCH/DELETE, кажется, не осталось. Я, по крайней мере, не сталкивался.

В любом случае, есть стандартный механизм обхода таких ограничений: использовать GET/POST (первый в случае не изменяющих состояние ресурса операций, второй — для изменяющих), а нужный реально метод передавать заголовком X-HTTP-Method.
В 2015 году большинство сервисов пишутся на HTTPS.
Тема для холиваров на каждый день :)
По-сути на http хорошо ложится более-менее стандартный CRUD, вопросы начинаются с более сложными вещами.

Как вы обычно делаете (отображаете в HTTP API) query сущностей? А есть query возвращает данные по нескольким сущностям сразу? POST или GET для query? Odata, свой DSL или jquery format для параметров запроса? :)

Вы предлагаете «query» задавать как параметр запроса к сущности (../etntity/?query) т.е. в самом URL path глаголов быть не должно? Нам, вот, часто нужны глаголы и засовываем мы их именно в path (entity/copy, entity/prefetch, entity/lock). От части это связно с выбранной библиотекой HTTP: так удобнее писать роутеры — они биндятся на конкрутный path, а вот разделять по query param пришлось бы внутри каждого роута, что неудобно. Как и на чем вы пишете роутинг? И насколько разнообразны потребители вашего API в плане платформ\языков? У нас service'ы на scala, апотребители C#, C++, python, javascript и typescript и у каждого свои претензии что «что-то» делать недобно (например дописывать ID в середину path). :)

И в общем, в каких случаях вы позволяете себе отойти от следования рекомендациям по формированию пары method + URL?
> Тема для холиваров на каждый день :)

Не совсем.
Про REST-архитектуру действительно можно много холиварить, но пост-то не про неё, а про то, что нужно понимать смысл используемых сущностей — чем PUT отличается от POST, 400 от 500, ресурс от вызова метода. Здесь, в общем, разногласий быть не может: нравится тебе эта архитектура или нет, но Сеть работает именно так.

> Как вы обычно делаете (отображаете в HTTP API) query сущностей? А есть query возвращает данные по нескольким сущностям сразу? POST или GET для query? Odata, свой DSL или jquery format для параметров запроса? :)

> Вы предлагаете «query» задавать как параметр запроса к сущности (../etntity/?query) т.е. в самом URL path глаголов быть не должно? Нам, вот, часто нужны глаголы и засовываем мы их именно в path (entity/copy, entity/prefetch, entity/lock).

Вообще, в парадигме REST ответ на каждый из ваших запросов такой: создавайте специальный ресурс для каждого специфического требования.
Нужна мультиплексация? Значит, сделайте ресурс multiplexer.
Хотите избежать глагола в path? Сделайте ресурс: не GET /entity/prefetch, а GET /prefetcher/entity. И биндинги писать проще станет.
И так везде, где стандартной номенклатуры вам не хватает.
Я не могу дать универсальной рекомендации типа «всегда кладите query в query». Нужно смотреть на семантику конкретной операции и укладывать в HTTP-термины.

> И в общем, в каких случаях вы позволяете себе отойти от следования рекомендациям по формированию пары method + URL?

В целом, я вовсе не являюсь фанатом REST-архитектуры, многие вещи в неё ложатся плохо. Я категорически против смешения французского с нижегородским: если отходить от рекомендаций RFC — то полностью. Скажем, переходить на JSON-RPC или, прости господи, SOAP.

В реальности же постоянно наблюдаются адские кадавры, в которых перемешано всё, что можно.
Немного обидно за SOAP, за что вы его так? :) Свои задачи он решает достаточно хорошо
Его просто почти никто не умеет правильно готовить. Когда появляется задача «синтегрироваться с вон теми ребятами, у них SOAP», то в 9 случаях из 10 не получится просто взять клиентскую SOAP-библиотеку и синтегрироваться. Обязательно что-то сделано… не так (вон, ниже привели пример про fault). И нужно наворачивать слои костылей. В общем, SOAP очень хорошо ассоциируется с болью и страданием.

Впрочем, если быть справедливыми, HTTP REST тоже мало кто умеет правильно готовить (с теми же вытекающими последствиями). Собственно, поэтому, видимо, и написали этот капитана пост.
Логично, что при интеграции, если кто-то не соблюдает спецификацию, то вероятно где-то будет боль :)
Его просто почти никто не умеет правильно готовить.
Похоже, не неумеют, а ленятся. И это, в виду повсеместности, говорит уже не о людях, а о протоколе, в частности, об удобстве его использования. Вот я это у вас прочитал, и тут же возникла ассоциация с программированием на Java без IDE: раз для правильности и красивости нужно вручную набивать «многабукаф», то никто не захочет делать это дотошно и правильно, а предпочтут как-то «срезать углы» и делать менее трудозатратно.
Такой вопрос: у нас есть метод, возвращающий текущее время (как пример). Логично, что он должен быть GET'ом, но при этом кешировать его явно не надо, ибо глупо. Решение в явном запрете кеширования со стороны сервера, или есть что-то умнее с учётом кеширования шарахалок по луне?
В общем-то, можно придумать много решений, зависит от того, насколько важно всегда получать значение в обход кэша.

Вполне валидно в этой ситуации использовать POST, например, т.к. это метод с максимально широкой семантикой.
Можно обязать клиента приписывать к запросу время по его собственным часам и/или просто рандомное значение, тоже никто не запрещает.
На практике заголовков запрета кэширования от сервера обычно достаточно.
POST — несемантично, рандомное значение — костыль. Но концепцию понял, волшебства тут нет :)
Извините, я не специально, но только сейчас заметил, что у вас кешируется время на yandex.ru/internet :)

Скриншот

Да, всё точно, ваш ответ про время закешировался, обновил браузер и получил
Скриншот


Идёт запрос на yandex.ru/internet/api/v0/datetime в ответе нет параметров кеширования. Мой вопрос оказался очень к месту, хотя был задан раньше, чем я увидел проблему у вас. :)
Забавно.
Передам разработчикам )
А еще, у вас там же по нидерландскому айпишнику определяет Реутов, исправьте, пожалуйста :)
Мы как-то для подобных случаев (у нас не должны были кешироваться динамические картинки) просто добавляли к таким URL параметр со случайным значением который ни на что не влиял.
Но тем не менее они все равно будут кешироваться забивая кеш бесполезными копиями…
т.е. перенесли проблему с больной головы на здоровую.
UFO just landed and posted this here
Еще есть вариант с сохранением результата запроса на сервере или кодирование его в урле для последующей отдачи уже по другому ресурсу. К примеру:
1 запрос
> POST /current_timestamp/
< 201 created
< Location: /timestamp/1440751720/

2 запрос
> GET /timestamp/1440751720/?format=с
< 2015-08-28T10:48:40+02:00
А это уже бредом попахивает. Конечно, семантичненько вышло, но накручивание могучей логики ради красоты семантики это уж слишком.
В таких случаях предполагается использование заголовка Last-Modified.
OPTIONS, он не модифицирует, и не кэширует. Вроде вписывается
Это был самый полезный Капитан Очевидность которого я когда-либо видел.
Прокси-сервера не очень охотно поддерживают что-то кроме get и post, особенно какие-нибудь корпоративные всеограничивающие.
10 пункт насилует логику. В заголовке написано что все что дальше — заблуждения. Т.е. в итоге получается что:
* 401 Unauthorized не обязан сопровождаться заголовком WWW-Authenticate (хотя выделение болдом намекает на обратное);
* статусы 3xx — это только редиректы;
* статус 404 клиент не имеет права повторять (кстати предложение сформулировано вообще неправильно: клиент имеет право повторять запрос а не статус).
UFO just landed and posted this here
Где-то минимум трети написанного — субъективщина. Начиная с первого пункта и далее. Чем вам не нравится метод в запросе? Почему не /Луна/?who=шарахалка?

Якорь ещё зачем-то фрагментом обозвали. И про сортировку по породам — тоже спорный вопрос, многие интернет-магазины уже отошли от этой концепции нулевых и ссылаются на один и тот же монитор и как /monitors/HP/model, и как /HP/monitors/lcd/model, фильтры и теги вместо жесткой каталогизированной структуры.
Не стоит брать за основу своих доводов некорректно написанные ядра интернет-магазинов. За разные запросы, возвращающие одинаковые сущности, можно и с поисковой выдачи улететь.
> Где-то минимум трети написанного — субъективщина.

К рекомендациям RFC и IETF можно много претензий предъявлять, но уж точно не в плане субъективности.

> Почему не /Луна/?who=шарахалка?

Потому что ресурс — то, над чем выполняется действие. Стреляя из пушки, вы выполняете действие над пушкой, а не над Луной. К тому же, вряд ли у вас есть ресурс «Луна».

> Якорь ещё зачем-то фрагментом обозвали.

Это не я, это Тим Бёрнерс-Ли
tools.ietf.org/html/rfc3986#section-3.5

> И про сортировку по породам — тоже спорный вопрос, многие интернет-магазины уже отошли от этой концепции нулевых и ссылаются на один и тот же монитор и как /monitors/HP/model, и как /HP/monitors/lcd/model

Тем самым нарушая самый базовый принцип архитектуры Web из возможных.

Although there are benefits (such as naming flexibility) to URI aliases, there are also costs. URI aliases are harmful when they divide the Web of related resources. [...] Good practice: Avoiding URI aliases. A URI owner SHOULD NOT associate arbitrarily different URIs with the same resource.

www.w3.org/TR/webarch/#uri-aliases
Теги/фильтры из путей кыш! Тип товара и производителя — тоже, ибо бывают комбинированные типы товара, бывает мультибрендовые товары, и если в пути есть бренд или тип товара, такие товары в него честно не положить. Так?
То есть, GET shop.example.com/catalog/article наше всё, а теги — идут в характеристики товара, и в URL им не место?
Зависит от конретной стуктуры каталога.
Общий вариант — отдельная страница товара типа /goods/{name}, и отдельный поисковый ресурс типа /search?tag=lcd, который умеет фильтровать по признакам (ну или несколько таких ручек под каждый тип фильтра).
А если переделать фразу с Get на Post:
«да плевать я на всё это хотел, придумали какой-то ненужной ерунды; как у меня работали все сервисы только на POST, так и дальше будут, мучайтесь со своими PUT-ами и DELETE-ми сами».
То что в ней не так?
Да практически всё.
— Нет кэширования.
— В условиях плохого соединения запросы повторять нельзя.
— При нажатии Back или Refresh браузер будет показывать сообщение «Подтвердите повторную отправку формы».
И так далее.
Если я делаю REST API для SPA или для мобильного приложения, то мне как раз не нужно кеширование.
Надеяться, что кто-то повторит мой запрос тоже как-то странно, для этого я делаю отдельный обработчик, хотя, тут, может и стоит заиспользовать GET.
Ну а проблема Back или Refresh для REST API не актуальна.
На счёт Get/Post понятно, но зачем усложнять REST API всякими PUT и DELETE, если достаточно POST и GET.
Вот как раз мобильное приложение — это то место, где *необходимо* использовать идемпотентные методы, поскольку мобильная сеть крайне ненадёжна, и любой запрос может отвалиться по таймауту (и при этом успешно выполниться на сервере).

Ну и как-то странно отказывать от кэширования в мобильном о_О
Зачем в мобильном REST API использовать PUT, DELETE, CREATE и т.п. если можно обойтись POST и GET?
В чём реальный профит?
Приблизительно в том же, в чем вообще профит от хорошей архитектуры — проще, понятнее, расширяемее.
«Хорошая архитектура» — слишком абстрактное понятие. Я предпочитаю конкретные вещи. Вроде того, что ответили выше про то, что GET-кешируемый и повторяемый. Ок, это может помочь достучаться до сервера. Хотя логику кеширования и обработки таймаутов и ошибок всё равно нужно писать на клиенте.

Вот, например, я реализовал публичный REST API, и мне надо чтобы клиенты могли им просто воспользоваться по простой доке и не трогали меня. По мне, так лучше предложить им 1 или 2 типа запроса, чем 5.
Как-то предложить людям постить JSON запрос на /FunctionName проще, чем заставлять их выставлять X-Header, тип запроса, а данные либо пихать в query, либо в body и прочее.
Kлиенты же потом прийдут мозг выносить: «почему мой запрос не проходит» из-за этой расширяемости архитектуры.
Вроде того, что ответили выше про то, что GET-кешируемый и повторяемый.

PUT тоже (может быть) повторяемый. И это очень удобно, когда вам надо создавать объект по нестабильному соединению.

Хотя логику кеширования и обработки таймаутов и ошибок всё равно нужно писать на клиенте.

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

По мне, так лучше предложить им 1 или 2 типа запроса, чем 5.

Эти «типы запросов» потом где-то выражать. У вас есть выразительное средство, но вы им не пользуетесь.

Вот, например, я реализовал публичный REST API, [...] Как-то предложить людям постить JSON запрос на /FunctionName проще

Просто это больше не REST, вот и все.

Есть такая штука — семантика. Вот вы ее нарушаете. Человеку, который привык к нормальным REST API будет сложно понять, что у вас происходит, и как этим пользоваться. Это как в SOAP вместо fault возвращать обычный ответ с кодом ошибки.
мне надо чтобы клиенты могли им просто воспользоваться по простой доке и не трогали меня
Как-то год работал с CMS, оптимизированной по такому принципу — было жутко неудобно и медленно.
Кстати, меня как-то на одном собеседовании спрашивали, зачем нужен GET, когда есть POST, который по идее должен делать всё то же самое. Сошлись на том, что из-за кэширования.
Вот меня уже давно мучает вопрос по custom http codes. При ошибках нам всегда нужно приложению отдавать конкретный номер ошибки, сейчас отдаем более подходящий стандартный http статус, а в теле ответа уже код конкретной ошибки. Боюсь за неадекватное поведение разных прокси у клиентов. Но такое дублирование разных кодов, сначала в заголовках, а потом в теле мне не совсем нравится. Есть у кого-то идеи по этому поводу или даже опыт работы с пользовательскими статусами?
Чтобы ответить на ваш вопрос, требуется конкретика. Почему вас не устраивает стандартная номенклатура http-кодов?

Если у вашего ресурса есть какая-то своя внутренняя номенклатура ошибок, то никто не мешает отвечать статусом 400 или 500 (в зависимости от того, клиентская ли ошибка) и отдавать в теле ответе, скажем, JSON с описанием произошедшей ошибки (в т.ч. внутренним кодом). Однако в такой ситуации я бы рекомендовал работать по RPC-протоколам, т.е. использовать HTTP как транспорт и отвечать строго 200.
Не хотел приводить конкретный пример, что бы не обсуждать частный случай. Но представьте, у нас есть игра, и некий ресурс, который возвращает актуальный статус игры: Фаза1, 2… 10. Хотелось бы, например, возвращать коды 251, 252… 260 соответственно. Тогда даже простым HEAD запросом можно уже знать состояние. Или другая ситуация с ошибкой валидации запроса: 471 — неверный параметр «А», 472 — конфликт значения «Б», и т.д.

никто не мешает отвечать статусом 400 или 500 (в зависимости от того, клиентская ли ошибка) и отдавать в теле ответе, скажем, JSON

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

Но к сожалению на данный момент нет общепринятого промежутка для пользовательских статусов, например 45X-49X;55X-59X;7XX-9XX;, или другой способ: конкретный номер через точку:400.ХХХ.

Наверное что-то не уловил, но почему не использовать X-Status?
Да, это действительно отличный вариант! Не хочу писать это слово, «но» это уже из области пользовательских заголовков. А работа лишь с http статусом была бы предпочтительнее.
Почему? Ведь HTTP-статус — это «транспортная» часть, статус доступа. А Вы хотите отдавать логическую часть приложения, смешать два уровня.
Согласен, так оно и есть. Но на данный момент, мне кажется, это вовсе не два изолированных уровня, особенно в контексте API ресурсов, ведь за частую, как раз приложение решает или это Bad Request, или Conflict, или что-то там ещё. Выходит эти уровни уже давно смешаны. Поэтому пора бы уже сделать окончательное смешивание.
torkve совершенно прав, так делать ни в коем случае не надо.
HTTP статусы должны индицировать статус самого обращения по HTTP. Если HTTP — просто транспорт до некоторого программного endpoint-а, то номенклатура HTTP-статусов должна описывать строго транспортную часть. 200 — запрос дошел и обработан endpoint-ом (независимо от успешности и состояния такой обработки), 4xx — запрос сформирован неправильно и до endpoint-а не дошёл, 500 — ошибка сети или веб-сервера (т.е. обработчика HTTP-запросов, не самого endpoint-а).
Неужели никто никогда не меняет напрямую, или косвенно статус код ответа в самом приложении? Если «нет», то тут вы меня удивите, а если «да», то речи о разделении транспорта и приложения уже быть не может. Или мы как-то друг друга не верно понимаем? Или ещё пример: Если endpoint по фазе луны определил что запрос не верный, неужели статус не измените на 400?
> Неужели никто никогда не меняет напрямую, или косвенно статус код ответа в самом приложении?

Ммм, это как?

> Если endpoint по фазе луны определил что запрос не верный, неужели статус не измените на 400?

Нет.
HTTP статусы — для ошибок уровня протокола HTTP.
Вас же не удивляет, что TCP не знает про ошибки HTTP, и статус его пакетов «ок», даже если в них передаётся закодированная 500-ая ошибка HTTP? Так и в вашем случае. HTTP отработал валидно — статус 200.
> Неужели никто никогда не меняет напрямую, или косвенно статус код ответа в самом приложении?
Ммм, это как?

Response.StatusCode = HttpStatusCode.BadRequest;
// или через exception
throw new HttpBadRequest(message);
// или ещё другими 100500 способами.

> Если endpoint по фазе луны определил что запрос не верный, неужели статус не измените на 400?
Нет.
Интересно, как же по вашему в этом случае приложение должно получить и отреагировать на ошибку (если мы говори о RESTful запросах и json формате)? lair упоминает о адекватном использовании существующих статусов. И действительно, все так делают, вы наверное первый от кого я слышу, что нельзя в приложении менять статус на 400, например.

lair, «адекватно-не-адекватно» это в любом случае «использование» в целях приложения, и я к тому и веду, что раз мы уже используем статусы, то чисто из мышления программиста у меня возник вопрос, ведь это абсолютно не DRY менять http-status, и ещё! дополнительно! передавать конкретный статус или в кастомном заголовке, или же где-то в теле ответа. И клиент тоже соответственно должен реагировать на http статус, a потом ещё на конкретный статус ответа.
Здесь какое-то недопонимание.

Если у вас REST-приложение, оно *обязано* отвечать HTTP-статусами, соответствующими типу ошибки.

Если у вас не-REST (JSON-RPC, SOAP, whatever) приложение, то good practice — отвечать не-200 статусами только в случае проблем самого HTTP-протокола (сетевые ошибки, веб-сервер упал). Все ошибки внутреннего состояния сервиса кодируются в соответствующий (JSON, XML) формат и возвращаются с HTTP-статусом 200.
Читал-читал ваш диалог, и окончательно запутался с REST и кодами ответа.

При запросе, например, профиля пользователя по id возможны варианты:

1. Пользователь найден, статус ответа 200, в теле ответа профиль в формате xml. Семантика xml описывается в доке на API.
2. Пользователь удален, статус 410, тело ответа пустое
3. Такой пользователь никогда не был зарегистрирован, статус 404, тело ответа пустое
4. Этот профиль запрашивающему смотреть нельзя, ответ 403
5. Пользователь в отпуске, отвечаем 303 и id заместителя

По-моему, это ерунда какая-то?
Если вы про REST, то никакой особой ерунды (разве что последнее вызывает у меня сомнения — во-первых, должен быть не ID, а Location, а во-вторых, что важнее, я не уверен, что семантика 303 совпадает с семантикой «замещения»).
Ерунда тут в том, что таким подходом можно описать только работу с чем-то, очень похожим на файловую систему: ссылки, каталоги, права доступа, вот это все, а содержимое файлов — просто блоб. Можно, конечно, притвориться, что наши сервисы (как профили пользователей в моем примере) — это такие хитрые ресурсы, но тогда от REST в такой схеме ничего не остается.
Зачем притворяться? Профили — и правда ресурсы, а то, что вы называете «файловой системой» — всего лишь иерархически организованные ресурсы с перекрестными ссылками. И это вполне понятное человеку структурирование домена.

Кстати, почему же содержимое — просто блоб? Нет, отнюдь, это гипермедиа.
Если у вас не-REST (JSON-RPC, SOAP, whatever) приложение, то good practice — отвечать не-200 статусами только в случае проблем самого HTTP-протокола

А где можно почитать про эти good practice?
ведь это абсолютно не DRY менять http-status, и ещё! дополнительно! передавать конкретный статус или в кастомном заголовке, или же где-то в теле ответа.

Почему же? Это типичная ситуация статуса и сабстатуса. Один клиент будет разбирать тело, другой — нет, но оба они получат ошибочный статус верхнего уровня и не будут считать сообщение успешным.
Выше я имел ввиду именно RESTful подход, не RPC, SOAP
Вообще, это вы странное сейчас описываете. Так (разделяя транспорт и бизнес) действительно делали в SOAP, а вот в REST так не делают. REST признает HTTP-протокол и использует его статусы для описания своего состояния.

Это не повод, конечно, заводить собственные статусы, но вот адекватно использовать существующие — например, 500 при любой серверной ошибке, которую мы согласились разглашать, или 400 в случае ошибки валидации, или 409, если изменилось состояние объекта для модификации — вполне разумно и адекватно.
Не очень понимаю, о чём вы. REST — это как раз предложенная идеологами HTTP архитектура веб-приложений, полностью на HTTP основанная. Разумеется, REST-сервис должен полностью и строго следовать описанным в стандарте принципам.

Я пишу ровно о том, что правильно или делать приложение в REST-парадигме, полностью следуя семантике HTTP; или делать приложение в другой архитектуре, например, SOAP или JSON-RPC, но тогда ни в коем случае не нужно смешивать статусы высокоуровневых операций с низкоуровневыми. HTTP 500 и HTTP 200 передаются через TCP совершенно одинаково, никто не пытается HTTP-статусы спустить на уровень TCP. Так же и ваше приложение поверх HTTP не должно использовать HTTP-статусы для индицирования своего высокоуровневого состояния.
Окей, признаю, не отследил момент, когда вы предложили отказаться от REST и перейти на RPC-over-HTTP. В этом случае, действительно, вы правы (другое дело, что я не могу найти достаточно мотивирующих факторов для такого перехода, но это тема другого обсуждения).
а где вы работаете, если не секрет? :)
Поскольку REST признает HTTP-протокол и использует его состояние, то он не исключает использование и собственных заголовков, ведь HTTP не запрещает этого делать (ну кроме префикса X- который записали в старевший).

Конечно приложение должно максимально использовать существующие статусы, но если логика приложения требует своих, то ни что не мешает их добавить. Просто отражать они должны нужные состояние приложения, а не соединения.
Вопрос с подковыркой. Где в приведенных примерах 3хх?
Я к тому, что endpoint как именно конечно точка htpp взаимодействия может и сам отвечать статусами.
3xx в этой ситуации возможен, кажется, только в случае переезда endpoint-а за другой URL
На вскидку 304. Веб сервер без участия endpoint не знает, можно ли так ответить (мы же говорим про приложение, т.е. запросы динамические и это не статика в ФС в которую веб сервер может сходить и сам).
Как правильно должно выглядеть такое взаимодействие:

клиент: POST /resource/
{ «method»: «getNyashnyKotikNextPhoto», «ifModifiedSince»: <таймстэмп> }
сервер: 200 OK
в теле либо { «notModified»: true }, либо фотка няшного котика

Неправильно:
клиент: GET /resource/?method=getNyashnyKotikNextPhoto
If-Modified-Since: <таймстэмп>
Сервер: 200 OK или 304 Not Modified

Почему так неправильно: потому что ручка resource обладает внутренней семантикой, неизвестной протоколу HTTP. В данном примере — сервер не только отдаёт/не отдаёт картинку, но и смещает внутренний указатель (getNextPhoto).

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

Я специально подобрал такой пример, конечно. Но, если у вашей ручки нет внутренней семантики, не ложащейся в HTTP — значит, можно строго по HTTP и работать. А если она есть, то на подобные грабли вы обязательно и наступите.
Ну и 1хх тоже остались охвачены. 102 без бэкнда никуда.
Вероятно, потому, что HTTP и HTML — разные вещи, и их даже разрабатывают три разные организации.
Вот кстати да, из html все эти навороты http не доступны фактически, то есть они нужны лишь небольшому подмножеству ваятелей собственных клиент-серверных API, не имеющих отношения у браузеру, но почему-то желающих пользоваться именно http и ничем сверх него. Странные это люди, кажись…
Говорят, XML HTTP Request бывает в браузерах. А в некоторых даже таки Fetch.
Отличная статья, спасибо кэпу :)

Но у меня есть вопрос: где посмотреть реальный пример, где все эти методы реализованы и правила соблюдаются?
Мне вот, например, не доводилось использовать такой API :)
У GitHub'а весьма недурной API, я думаю, что там выдержаны как минимум почти все пожелания из этого поста (кроме Capability URL, но они не к API относятся, а к веб-интерфейсу): developer.github.com/v3
Спасибо, посмотрю. Как обычно, все под носом, но не заметно :)
UFO just landed and posted this here
Видимо API какого-то из сервисов Яндекса.
Здесь? КМК хабрахабр использует бОльшую часть из этой статьи =)
Опять полное непонимание, что такое URL/URI/URN… бррр
И это в Яндексе?
Последний абзац можно отнести к любым стандартам и соглашениям — они для того и есть, чтоб все понимали друг друга.
Оформить бы эти рекомендации в виде онтолигии… Или, хотя бы, таблички.
сильный генератор случайных строк (например, UUID)

Это вы так пошутили? UUID не является криптографически стойким рандомом, от слова «вообще»

Теорию, почему так — читайте в вики. А чтобы убедиться на практике — сделайте:
SELECT CONCAT(UUID(), '|', UUID())

И получите что-то вроде:

f24bdc1a-5082-11e5-b94b-001d92633d17
f24bdc47-5082-11e5-b94b-001d92633d17

А повторный вызов минут через пять даст:

2a3b5871-5083-11e5-b94b-001d92633d17
2a3b589e-5083-11e5-b94b-001d92633d17

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

Для тех, кто не понял, практическая реализация атаки:
  1. Делаем восстановление пароля «на себя» — запоминаем время запроса
  2. Делаем восстановление пароля на «цель» — запоминаем время
  3. Вычисляем примерное значение для цели по разности времени между запросами
  4. Брутфорсим по значению п.3 +- #ffff


Резюме: НИКОГДА не используйте uuid как секретную строку!

Ну, и раз сказал «как не надо» — в качестве дополнения, простой способ получить нормальный криптостойкий секрет на ПХП (с переводом в шестнадцатеричную строку)

bin2hex( openssl_random_pseudo_bytes( 16 ) )
получите что-то вроде:

f24bdc1a-5082-11e5-b94b-001d92633d17
f24bdc47-5082-11e5-b94b-001d92633d17


Это конкретно ваша реализация GUID так себя ведет. Сравните:

CE46945F-61F2-4D52-96F1-5F57D99EC519
31FB8849-CC81-460B-B873-58CEA0E7443E
04298C79-8B33-4C44-8314-CA6A7F27ED4D
813F0486-3C04-45D9-8D63-B5131DC75708


(это четыре последовательных вызова генерации GUID на Windows, конкретно — newid() в MS SQL)

Функция, конечно, все равно не годится для сильной криптографии, но вот реально предсказать ее значения за разумное время вам будет непросто. А ведь еще может быть не один хост.
К сожалению, вы не правы. Значения только выглядят рандомными, но таковыми не являются.
Хотя реализация GUID в Windows проприетарна, тем не менее, есть исследования показывающие ее уязвимость.

Сама по себе природа UUID такова, что генерируемые значения не будут иметь криптографически-нормального распределения, т.к. uuid должен обеспечивать уникальность, а не случайность.
А это совершенно разные вещи!
Не прав в чем? Я сказал, что они случайные? Я сказал, что они имеют распределение, неотличимые от случайного?

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

(BTW, вполне возможно генерить криптографически сильные (122 бита из 128) GUID-ы, удовлетворяющие четвертой версии стандарта)
Вы написали:
Функция, конечно, все равно не годится для сильной криптографии, но вот реально предсказать ее значения за разумное время вам будет непросто.


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

Для тех, кому неохота вникать в статью целиком, приведу несколько ключевых моментов:

Сейчас мы уже знаем, из какого теста сделан Uuid-генератор и с уверенностью можем сказать — испытания на backward security он не выдерживает. Полное обновление состояния у него возникает спустя 4·106 байт выхода (8 экземпляров RC4 * 500 000 байт), поэтому злоумышленник, сумевший заполучить «слепок» всех регистров и S-блоков UuidCreate в определённый момент времени, способен предугадать до 250 000 будущих Uuid-значений. Более того, источником энтропии генератора выступает ГПСЧ Windows в лице SystemFunction036, что, с учётом предсказуемости последнего [8], увеличивает длину скомпрометированного потока данных до запредельных 256·106 байт!


Вы спросите, зачем потребовалось вместо источников энтропии ОС использовать суррогат, получаемый из ГПСЧ Windows? Начну издалека. Во-первых, никто в здравом уме на стойкость UuidCreate больших надежд не возлагал (а те, кто возлагал, просто обязаны дочитать эту статью до конца). Сами демиурги вполне внятно обозначили свою позицию в RFC4122[1]: «Do not assume that UUIDs are hard to guess; they should not be used as security capabilities (identifiers whose mere possession grants access), for example. A predictable random number source will exacerbate the situation». Так что перед разработчиками никогда не стояло цели создать безопасный Uuid-генератор. К сожалению, планету Земля все ещё топчут гоблины (не только топчут, но и карты экспресс-оплаты штампуют!), которые к подобным предостережениям относятся без должного трепета.


Как видите, даже в стандарте, четко сказано — что uuid нельзя использовать как «секрет» — так как ее значения «угадываемые»

Я вот прямо даже не знаю, что еще можно добавить, если уж разработчики стандарта однозначно пишут, что "should not be used as security capabilities (identifiers whose mere possession grants access)"…
Однако, приведенная по ссылке работа четко показывает, что это просто.

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

Впрочем, это не так критично. Для «честных» capability urls действительно лучше использовать криптографически сильные GUID.
Сколько — можно предполагать с очень высокой точностью. Посмотрите пример атаки-же… Вы сами формируете оба запроса, «эталонный» и «хакерский».

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

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

Вот только (а) вы на сервере не единственный пользователь (б) генератор гуидов — если люди не заморачивались с безопасностью — один на всю систему, а это значит, что он используется в каждой операции, а не только в атакуемой

А ещё (ц) вы должны знать, какая реализация используется. Мы вот используем gen_random_uuid() из модуля pgcrypto для PostgreSQL. Хоть в исходниках там и используются псевдослучайные числа (не иначе, как для производительности), но для целей генерации Capability URLs этого должно быть более, чем достаточно.
Речь идёт о UUID версии 4. Исправил в тексте, спасибо.
Как-бы формально вы правы.

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

А еще лучше — приведите примеры практической реализации. Думаю, это будет полезно всем, так как тема на самом деле «больная». Ну очень уж часто я встречаю генерацию «секретов» с использованием uuid, к сожалению…
У меня вот другой взгляд на шараханье по Луне. Из REST и соответственно HTTP мы знаем, что кошерно выполнять операции CRUD над ресурсами. Когда мы выполняем
POST /шарахалка/?to=Луна

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

Я бы делал либо так
POST /шарахалка/выстрелы/?to=Луна

ну или может еще так
POST /луна/ущерб?weapon=шарахалка


Т.е. всегда фокусироваться на том, что мы создаем или меняем, и использовать это в качестве ресурса а не инструмент или фабрику, с помощью которых это делается.
forgotten давно хочется почитать то же самое, но с учётом современных реалий — HTTPS. А то все про долбаное «старьё» в виде ответа на GET кеширующим прокси пишут и пишут всё. Хочется вычеркнуть из поста 80%, не относящееся к HTTPS. Кстати, тема для отдельного поста…
Во-первых, HTTPS исключает кэширование на стороне промежуточных сетевых агентов. Но вот конечный клиент и имеющиеся на его стороне агенты, будь то браузеры/месенджеры/иные приложения (и плагины/аддоны к ним!) или используемый разработчиком фреймворк, имеют прямой доступ к данным, и все указанные спецэффекты пронаблюдать довольно тривиально.

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

Аналогично и на стороне поставщика данных: HTTPS может начинаться с внешней границы, на которой стоит простая железка с большим кэшом.
Да, и это тоже правда. Далеко не все гоняют трафик внутри ДЦ по HTTPS, часто внутренние прокси по HTTP работают.
Пожалуй, с 80% я перегибаю, но вот если речь идёт о том, что вы можете контролировать (nginx и другие reverse-proxy) и параметры кеширования в браузере — то всё-таки отличия существенные. Если у тебя https и куки солёные, ответ от своего сервера получишь всегда, независимо от метода.
Самое удивительное в этой статье то, что в блоге Яндекса кто-то использует матюки для большей доходчивости :)
Но придраться не к чему — на то он и Капитан, чтобы использовать армейскую лексику (хотя бы чуть-чуть).

а как лучше отвечать на delete если удаляемый ресурс отсутствует: 204 или 404?

С точки зрения протокола — 404.

не будет ли это нарушением идемпотентности delete? вызываем 2 раза подряд, первый раз получаем 204, второй — 404

Строго говоря не будет — состояние ресурса-то не изменилось.
А вообще в таких ситуациях лучше передавать с клиента ключ идемпотентности или последнюю известную ревизию/ETag ресурса. Тогда сервер сможет сделать вывод: это клиент повторил запрос, и можно ответить 200; или на клиенте какая-то ошибка в коде, и надо отвечать 4хх.

Есть специальный код 410 Gone для случая, когда известно, что ресурс раньше был, но «его ушли».

В реальной жизни его редко употребляют из соображений безопасности.
Sign up to leave a comment.