А, ну если у вас "заказ" это просто новое состояние корзины, и вы переводите её в это состояние путём PUT или PATCH запроса на сам ресурс "корзина", то да, 409 тут логичен.
Я имел ввиду случай, когда "заказ" — это отдельная от корзины сущность.
Из воздуха что-ли получил? Или через HTTP/2 Push-и прилетел? Клиент должен сам попытаться, что-то изменить на сервере, что бы получить ошибку. В ваших кейсах клиент не коризну меняет и не цену для товара. А создаёт новую сущность "заказ". С чего бы тут быть конфликту. Новое состояние заказа не может конфликтовать со старым, потому что на сервере нет вообще заказа с его старым состоянием.
PS: Про то как может измениться корзина или товар на сервере без ведома или участия клиента — это я прекрасно понимаю.
К стати мое понимание REST-а не требует обязатаельного использования HTTP кодов
Да, тут всё верно. Но HTTP — это уже готовая система с архитектурой построенной в соответствии с принципами REST. И у вас есть возможность использовать готовое решение, которое описано в спецификациях, и ваш продукт смогут быстро понять те, кто знает HTTP. Или вы можете сами придумать, что-то своё и тогда вам придётся писать документацию и объяснять на каких принципах вы строили свою архитектуру, и как в ней интерпретировать ошибки.
Про 409 я предпочитаю считать, что это применимо только в случае, когда мы явно пытаемся изменить на сервере существующий ресурс, на другую его версию, которая конфликтует с текущей (например передали старую версию).
"The 409 (Conflict) status code indicates that the request could not be completed due to a conflict with the current state of the target resource."
В ваших кейсах это не так. В них идёт попытка создать второй ресурс другого типа (заказ), который каким-то образом зависит от первого (корзина или товар). Я не считаю что тут есть "конфликт" с созданием самого заказа. Тут есть либо запрет на создание заказа из-за текущего состояния всего приложения — это код 403. Или можно интерпретировать как ошибку в параметрах создания заказа (когда клиент передал не валидное значение цены товара, или номер версии корзины) — это 422.
Ещё можно было бы использовать статус "412 Precondition Failed", если придумать свой кастомный вариант условного заголовка, например: If-Price-Match: product1=100$, product2=20$
Но это уже экзотика.
Насчёт 403 и аутентификации "средствами HTTP" — ваша правда. У меня глаза "замылились" описанием этой ошибки из русской википедии.
А то что никто не реализует 401 и аутентификацию как это написано в спеках — это опять же не вина самого HTTP (может мне конечно показалось, но я решил, что вы придерживаетесь мнения, что HTTP сам по себе бесполезен, и годится только как транспортный протокол).
Никто не запрещает вернуть эту информацию клиенту любым удобным способом. Но скорее всего будет лишним стандартизировать этот способ, поскольку для этого нет пока особых причин. Что с этой информацией будет делать прокси общего назначения, которая не знает особенностей конкретного приложения? Во многих случаях такая детализация не нужна даже клиентам.
никто не прибил гвоздями это требование
Не хочу сейчас искать цитаты из дисера Филдинга, может я реально что-то не так понял. Но поверьте моему опыту — HTTP был сделан именно с такими идеями (а делался он по таким же принципам о которых писал Филдинг). Это очень хорошо видно, когда пытаешься работать с ресурсами через HTTP так как это в нём задумано — всё сразу становится на место, и нет ни каких не разрешимых затыков, и классификация ошибок оказывается вполне достаточной.
А если вы идёте "в чужой монастырь со своим уставом", то у вас и выходит что всё не так, и всё криво, и мало детализации и не понятно как использовать. В этом случае действительно надо использовать HTTP только как транспортный уровень и обходится только GET/POST-запросами (если действовать последовательно то надо только POST, но вы наверняка захотите кеширование сторонними решениями), и двумя кодами ответа: 200 — запрос обработан, 500 — запрос не обработан.
большинство просто не заморачивается детальной диагностикой и классификацией
Вы противоречите сами себе. С одной стороны вы хотите "больше деталей", но при этом согласны с тем что никто не будет с этим заморачиваться. "Ленивые" разработчики даже не хотят использовать сделанную и описанную систему классификации ошибок в HTTP. И начинают выдумывать свою классификацию. Но винят в этом почему-то HTTP с его "недостаточно богатой системой ошибок".
Большинство претензий к статусам HTTP-ответов порождается узкостью мышления разработчиков. У разработчика ошибка в ТЗ называется "Нельзя создать пользователя с возрастом < 0". Разработчик смотрит в таблицу кодов HTTP и возмущается: "Как так, где ошибка 'XXX User is not born yet'? Говно ваш HTTP!".
пока HTTP используется по прямому назначению — как транспорт.
Выдержка из википедии:
"HTTP — протокол прикладного уровня передачи данных, изначально — в виде гипертекстовых документов в формате HTML, в настоящее время используется для передачи произвольных данных."
Так что всё-таки прямое назначение HTTP — прикладное, а не транспортное. Иначе зачем там столько HTTP-методов с конкретной семантикой, различные статусы ответов, прикладные HTTP-заголовки? А в качестве транспортного уровня можно приспособить что угодно, хоть светодиод от жёсткого диска — зависит от вашей задачи и личных предпочтений.
404 — нет объекта операции или нет endpoint? А если операция затрагивает больше одного объекта?
Если бы вы немного внимательнее читали про REST, то знали бы что в нём на одном URL может находится одна сущность. И быстро бы поняли, что у вас что-то не так, если вы пытаетесь одним URL-ом обозначить несколько несколько сущностей. Как вы заметили — даже 404 не понятно как возвращать в данном случае. Я про это уже написал — если у вас ошибки не мапятся http-статусы, значит вы отклонились от ограничений REST и HTTP. В REST поверх HTTP есть один простой шаблон для операций над множеством сущностей. Это делают посредством дополнительной сущности, назовём её "выполнятор", с помощью которой создают сущность "задача". В рамках каждой задачи сервер может выполнять любые действия, которые не "влезают" в простые CRUD-операции с одной сущностью. В такой реализации отпадает проблема "как вернуть 404 если операция затрагивает больше одного объекта". В этом случае эта информация возвращается либо как ошибка 422 на запрос к "выполнятору", и в теле перечисляют список ресурсов, которые клиент передал неправильно. Или в теле сущности "задача", если отсутствие ресурсов было определено в момент её выполнения, а не в момент валидации параметров её создания.
Очевидно, что в случае кодов HTTP (и даже дополнительных заголовков), их явно недостаточно даже для этого минимума.
Для быстрой классификации типа ошибки и какой-то её обработке промежуточными агентами (например кеширование) этого более чем достаточно. А все остальные подробности интересны (и будут понятны) исключительно клиенту. А клиент может себе позволить распарсить тело ответа и получить от туда информацию в любом формате, который удобен для конкретной системы.
Неужели Вы верите в возможность реализации стандарта, который сможет описать совершенно любые детали ошибок? Ещё и так что бы их смогли всегда понимать любые сторонние решения, которые работают с протоколом HTTP (прокси, мониторинги и т.п.)? Всегда найдётся кейс, которому будет не хватать этого супер-стандарта.
на 429 и на разные виды 403-х вполне можно ожидать реакции от прокси.
С 429 согласен, что можно придумать проксю, которая будет DOS-ить сервер за клиента. Правда клиент про это ничего не узнает и возможно даже отвалится по таймауту и сообщит пользователю, что сервер плохой и тормозной, вместо того что бы попросить его не посылать часто запросы.
Что делать проксе с 403 я не представляю. Сервер вполне конкретно дал понять: "Обрабатывать этот запрос не буду, потому что запрещено". Разве что прокся пошлёт запрос с другого IP адреса, если сервис использует авторизацию на основе диапазона IP адресов. Но это уже очевидно будет не прокся общего назначения, а какая-то специализированная, которая знает про специфические методы авторизации на сервере.
На примере банального интернет-магазина:
заказ уже был создан — зависит от ситуации. Например, если ресурс создаётся методом POST (т.е. клиент не знает URL нового ресурса, и просит другой ресурс выполнить его создание) я бы мог вернуть код 200 и в теле ответа уже существующий ресурс (у нас создание нового ресурса обозначается статусом 201 Created). Но это в случаях когда допустима "дедупликация". Тогда клиент понимает, что новый ресурс не был создан с нуля (обычно ему это и не важно) и сразу же получает данные по существующему ресурсу без дополнительных GET запросов. В этих случаях обычно клиенту всё равно до особенностей хранения данных на сервере — ему важно только, что бы на сервере был ресурс с требуемыми характеристиками.
На моей практике очень в редких случаях приходилось использовать 409. Судя по спецификации это применимо в кейсах, когда два запроса пытаются изменить один и тот же ресурс (т.е. они знают его URL), и при этом второй запрос посылает более старую версию ресурса, который создал/изменил первый запрос. Т.е. эта ошибка предполагает, что в запросе будет информация, по которой сервер сможет определить "версию" ресурса, и запретить перезаписывать имеющуюся у него более новую версию. Статус 409 сообщает клиенту, что он должен синхронизировать своё состояние с сервером, запросив с него новую версию ресурса (и он может это сделать, т.к. знает его URL).
заказ нельзя создать, потому что не разрешается иметь более N заказов одновременно — я бы перефразировал "заказ запрещено создавать" и вернул вполне очевидный в этом случае 403 с пояснением причины в теле ответа.
заказ нельзя создать, потому что товар в корзине закончился — выше я уже описал область применения кода 409. Тут он не очень подходит, т.к. скорее всего клиент не знает URL "заказа", а потому и не может быть конфликта с его изменением. Тут может подойти 403 — запрещено создавать заказ на основе пустой корзины. Или 422 — клиент передал недопустимый (устаревший или не существующий) ID-версии корзины (это так же решает проблему, когда клиент видит у себя 2 товара в корзине, а на сервере их уже 3 или это 2 других товара).
заказ нельзя создать, потому что цена товара изменилась — похоже на предыдущий пункт. Клиент не пытается изменить существующий заказ, что бы был "конфликт". Он просто послал не правильные, устаревшие данные. Очевидно, для реализации такого кейса, клиент должен посылать либо ID-версии товара, или пару [ID-товара, цена], что бы сервер смог сопоставить ожидания клиента с тем что есть у него. Я бы вернул 422 с указанием что или версия товара устарела, или его цена указана не правильно.
практический пример: протухание токенов
Если авторизация выполняется средствами HTTP и запрос "запрещён" именно из-за авторизации, то надо отвечать кодом 401. Код 403 из-за авторизации возвращают только тогда, когда авторизация делается не средствами HTTP. Т.е. не через заголовок Authorization или другие заголовки, URL или параметры в теле запроса. А например авторизация по IP-адресу или SSL сертификату.
как сделать, чтобы мониторинг начинал орать
Логировать важные ошибки непосредственно в самом приложении и настроить мониторинг этих логов, а не только смотреть на access-логи, например nginx-а, который стоит перед приложением. Или использовать access-логи, которые пишет само приложение — ему то ведь известны все детали и оно может добавить их в свои access-логи.
Ну а кейс с протухшими токенами я выше разобрал — если бы возвращали 401, то наверное было бы немного проще фильровать эти ошибки.
а что собственно клиенту или прокси делать с ошибкой.
А каких действий вы ожидаете от абстрактной прокси, в ответ, например, на ошибку, когда клиент передал значение выходящее за пределы допустимого? Прокся что ли должна исправить это значение и повторно послать запрос серверу? Максимум чего бы я ожидал от адекватных промежуточных "агентов" в плане обработки ответов — это кэширование этих ответов. А для этого вполне достаточно той информации, которая передаётся в заголовках и описана в RFC. Не вижу пока причин придумывать что-то совершенно новое.
когда 404-ки были возвращены ошибочно, но клиент их закэшировал, тем самым продлив факап на неопределённое время.
Если тут 404 заменить на 200, то ситуация кардинально не поменяется. Поэтому в той ситуации, которую автор "разгребал", в большей степени виноват не "дизайн" кодов, а сервис, который ошибочно возвращает не те коды.
не существует платформы, которая из коробки умеет строить графики по семантическим данным в ответе ошибки, а не по статус-кодам.
Как говорится: "Если бы знал где упаду — соломку подложил". На все случаи жизни не придумать универсального решения. Если вам нужны специфические "графики" по какой-то частной проблеме, то вам придётся позаботься об этом самому. И для этого не обязательно выдумывать новые http-код. Гораздо проще в том месте где случилась ошибка, отправить подробности о ней в логи или в сервис типа Sentry, а не пытаться "угадать" ошибку по HTTP-ответу, предназначенному в первую очередь для клиента, и только во вторую — для мониторинга. Существующих кодов в HTTP вполне достаточно, что бы описать класс любой ошибки, которая может случится в системе построенной на принципах REST. Если же почему-то не получается "смапить" ошибку на имеющиеся коды, то очень вероятно, что вы отклонились от REST (или дополнительных ограничений HTTP, накладываемых поверх него). Я за несколько лет разработки так называемого RESTful API не встретил случая, когда не получилось подобрать стандартный HTTP-статус для классификации конкретной ситуации.
подход #3 весьма дорог в реализации.
Любая качественно сделанная система будет дорогой в реализации (или её будут долго делать). В случае с правильным использованием HTTP и принципов REST сложность заключается, как мне кажется, в том что эти "штуки" не бьют моментально разработчиков по рукам, как, например, компиляторы языков программирования. Без опыта и знаний можно как в С играться с указателями, и всё будет даже работать. Косяки или вообще не заметят из-за незначительности системы. Или это случится сильно позднее, когда автор, вероятнее всего, уже уволится. И тогда такая система либо уйдёт на покой или за неё возьмутся дорогие специалисты и с матами будут исправлять.
Могу сказать, что лично мне "не дорого" использовать подход #3. Но это конечно будет лукавство, т.к. я относительно дорогой специалист. И у меня ушло несколько лет на то, что бы научится "варить" HTTP и REST по "заветам" Филдинга. У нас даже HATEOAS есть. И я на практике вижу плюсы всего этого.
стандартизировать дополнительные машиночитаемые данные в ответе, предпочтительно в форме заголовков HTTP
Сомневаюсь что это практично. Как я уже написал выше — нет серебряной пули. Сложно будет придумать единое решение для более подробного описания конкретных ситуаций. Тех же HTTP-статусов довольно много. Плюс есть ещё различные стандартные HTTP-заголовки, которые дополняют статусы. В совокупности этого уже достаточно для большей части задач обработки запросов и ответов агентами находящимися в промежутке между сервером и клиентом. А уже на клиенте и сервере можно "договорится" и реализовать дополнительную детализацию так как удобно в конкретном случае. Хоть в теле запроса/ответа, хоть в заголовках.
PS: Не скажу что эта статья плоха. В ней есть доля правды и видны попытки разобраться. Хотя есть и чисто технические косяки в плане понимания предназначения конкретных статусов ответа и для чего они подходят, а для чего — нет. К сожалению в интернете не мало статей и заметок с такими и другими ляпами. И это совсем не улучшает понимание HTTP и REST среди разработчиков. Скорее даже наоборот — увеличивает предвзятое мнение, что всё это бесполезные знания, не применимые на практике, и надо просто всегда использовать, например, RPC через GET/POST и 2-3 HTTP-статуса для ответов.
Но фишка в том, что несмотря на всё, у HTTP есть заметный плюс — подробная и продуманная спецификация, которая уже не одно десятилетие применяется на практике. А у многих "протоколов мечты" либо нет спецификации, либо они не так сильно распространены.
Любой web-разработчик знает HTTP хотя бы минимально. И может легко найти спецификации и другие материалы про HTTP и разобраться как всё это можно использовать. Почти все знают что означают коды 200, 404 и 500. И это уже неплохо для начала — можно вести диалог на одном "языке".
2+2=4 в этом поле так же, как и в поле привычных нам, действительных чисел… (вообще сложение и вычитание для конкретно этого поля это просто побитовый XOR).
Что-то у вас не сходится. 2 xor 2 равно 0, а не 4.
По себе могу судить, что меня больше коробит не то, что отказываются от буквы Ё как таковой, а то что её заменяют буковой E, полностью игнорируя то что Ё обозначает совсем другие сочетания звуков. Было бы меньше претензий если бы вместо Ё писали 'йо' или 'ьо' (после согласных). Но выбрали какое-то половинчатое решение — ни туда и ни сюда.
Судя по первому вашему упоминанию, от буквы Ё изначально отказались книгопечатники. И есть мысль, что это случилось не из-за каких-то академических причин, а из-за "чувства красоты" и банальной экономии (зачем делать две разные буквы, если можно одну).
Но меня (а я учил читать своих двоих детей) просто бесят тексты без буквы Ё. Кто-бы чего не говорил, но если человек не знает слово, в котором должна быть Ё, то он никогда его не прочитает правильно. Он всегда будет произносить Е. И конечно же есть "одинаковые" слова которые отличаются по смыслу только за счёт разницы между E и Ё.
Даже моя пожилая мама неправильно читает тексты с современными терминами, которые появились в последнее 10-15 лет. Приходится её поправлять, и говорить где нужна буква Ё.
В общем я считаю очень вредным отказ от буквы Ё в массовом книгопечатании. Хорошо хоть школьные учебники ещё не "пробили дно", и используют эту букву.
К сожалению GIL не избавляет от необходимости следить за параллельным доступом к вашим данным. GIL защищает от параллельного доступа исключительно внутренности самого интерпретатора. А данные вашей программы он защитить не может, т.к. не знает где и как они используются.
asyncio.run() выполняет корутины, я никогда не заставлю обычную функцию выполнятся асинхронно, так что по сути не решает проблему никак
А как необходимость запускать блокирующую функцию в асинхронном окружении соотносится с вашей исходной задачей? Изначально вы поставили задачу — написать код один раз так, что бы его можно было использовать либо в асинхронном окружении, либо в синхронном. Тут совсем нет речи про то, что надо из асинхронного окружения вызывать какой-то блокирующий код. Это совсем другая задача.
В исходной задаче асинхронную версию библиотеки можно использовать в качестве основной и базовой реализации. А работу с ним из синхронного окружения либо переложить на пользователей (пускай сами явно используют asyncio.run() или loop.run_until_complete()). Или сделать какой-то декоратор, который ко всем вашим функциям и методам добавит синхронный враппер, использующий внутри себя asyncio.run(). Например такой враппер можно сохранять в свойство самой функции, что бы вызывать это как-то так:
my_async_func.sync()
PS: Стоит заметить, что asyncio.run(), вероятно, будет добавлять какой-то оверхед на создание евент-лупа и его удаление при каждом вызове ваших методов из синхронного окружения. Поэтому требуется тестирование. Как вариант можно попробовать реализовать однократное создание евент-лупа для вашей библиотеки с помощью какой-то функции для "настройки" работы библиотеки в синхронном окружении. И в дальнейшем использовать loop.run_until_complete().
Насчёт RESTfull я с вами согласен — это просто абстрактный термин, которым принято обозначать архитектуру приложений, якобы сделанную по принципам REST. Особой конкретики в этом нет, т.к. следуя принципам REST можно сделать разные архитектуры. По моему опыту 95% разработчиков, к сожалению, вообще не понимают REST, но при этом пытаются делать приложения и писать статьи про всё это. В результате интернет заполнен кодом и текстами не соответствующими принципам REST. И это только ещё больше запутывает, особенно новичков.
Но тема null-ов и отсутствующих полей не имеет отношения к REST как таковому. Просто есть 3 разных состояний поля в сущности: поле есть и имеет значение, оно null или его вообще нет. И эти состояния можно использовать в разных целях, в зависимости от контекста. Какие из них использовать и где — обычно понять не сложно, просто не забывая, что это действительно 3 разных состояния, которые лучше не смешивать. А потом задаться вопросами: какие из этих состояний допустимы, что каждое их них означает в конкретном случае. И если получается, что два разных состояния означают одно и то-же, то это повод хорошо подумать, действительно ли именно это вы хотели.
Про restfull вы несколько заблуждаетесь. Нет такого "правила" что возвращать надо то же что получил. Можно вообще получать в json, а отдавать в xml с другим набором полей. И в rest передают в обе стороны не сущности, а их некую репрезентацию. Какая при этом будет репрезентация — дело ваше, лишь бы её было достаточно для решения задач.
Но естественно часто просто удобнее если набор входных параметров является подмножеством результирующих свойств созданной сущности.
Возвращать только то что изменилось — это не удобно, нарушает схему ответа (разработчики клиентов вам точно спасибо не скажут), и накладно — если сущность была изменена другим клиентом, то придётся запрашивать её заново полностью, т.к. ваш запрос на изменение вернёт только то что он изменил.
Тут обязательно надо разделять "схему" входных параметров, и "схему" того что возвращает API (о чём говорится в статье). Входные параметры могут иметь более свободную форму. Например можно сделать так, что отсутствие в запросе поля или если оно равно null, будет преобразованно десериализатором-валидатором в "значение по умолчанию" (пустой массив).
А вот в схеме возвращаемого результата лучше избегать вольностей. Если поле должно быть всегда массивом — всегда возвращать массив, даже если по каким то причинам этого массива нет в хранилище данных.
Из моего опыта null нужен обычно только в тех случаях, когда он уместен в конкретной ситуации и не может быть иначе выражен конкретным типом данных. Например у типа datetime нет ни какого подходящего значения, которое бы можно было во всех ситуациях использовать в качестве "нет значения". Тут только null подходит. Для целых чисел иногда можно использовать 0, если бизнес-логика не допускает использование нуля в качестве нормального значения. С пустыми строками аналогично.
В случае коментов пустой массив это нормальная абстракция для "ещё нет коментов". Хотя бы потому что коменты надо куда-то добавлять. В null их не добавишь, а потому это не очень подходящее значение. Только если не использовать его как признак того, что коменты запрещены и их нельзя добавлять вообще.
Я лично стараюсь избегать использовать null без необходимости, т.к. он добавляет проверок в программе.
Что и как надо посылать при создании сущности — зависит от того нужено этоили нет. Если null допустимое значение для поля, то почему бы его не послать в POST запросе. Но можно так же сделать null как дефолтное значение поля при создании сущности. И тогда это поле можно вообще не посылать.
Не надо пытаться экономить на спичках и интерпретировать одно через другое.
Пустая строка — это всё ещё строка, а не null и тем более не отсутствие поля. Тоже самое и с пустым массивом.
Null не тоже самое, что отсутствие поля. Например, в запросе на обновление сущности, передав в поле expires_time значение null мы сообщим что ресурс больше не имеет срока жизни. А если вообще не передать поле — значит его не надо менять, а оставить текущее значение.
А нельзя что-ли сначала к МКС потихоньку подключать новые модули с одновременным выводом из эксплуатации и "сбросом" в океан старых модулей, пока вся станция не обновится? Обязательно надо сначала всё старое разломать, и потом с нуля строить?
А, ну если у вас "заказ" это просто новое состояние корзины, и вы переводите её в это состояние путём PUT или PATCH запроса на сам ресурс "корзина", то да, 409 тут логичен.
Я имел ввиду случай, когда "заказ" — это отдельная от корзины сущность.
Из воздуха что-ли получил? Или через HTTP/2 Push-и прилетел? Клиент должен сам попытаться, что-то изменить на сервере, что бы получить ошибку. В ваших кейсах клиент не коризну меняет и не цену для товара. А создаёт новую сущность "заказ". С чего бы тут быть конфликту. Новое состояние заказа не может конфликтовать со старым, потому что на сервере нет вообще заказа с его старым состоянием.
PS: Про то как может измениться корзина или товар на сервере без ведома или участия клиента — это я прекрасно понимаю.
Да, тут всё верно. Но HTTP — это уже готовая система с архитектурой построенной в соответствии с принципами REST. И у вас есть возможность использовать готовое решение, которое описано в спецификациях, и ваш продукт смогут быстро понять те, кто знает HTTP. Или вы можете сами придумать, что-то своё и тогда вам придётся писать документацию и объяснять на каких принципах вы строили свою архитектуру, и как в ней интерпретировать ошибки.
Про 409 я предпочитаю считать, что это применимо только в случае, когда мы явно пытаемся изменить на сервере существующий ресурс, на другую его версию, которая конфликтует с текущей (например передали старую версию).
"The 409 (Conflict) status code indicates that the request could not be completed due to a conflict with the current state of the target resource."
В ваших кейсах это не так. В них идёт попытка создать второй ресурс другого типа (заказ), который каким-то образом зависит от первого (корзина или товар). Я не считаю что тут есть "конфликт" с созданием самого заказа. Тут есть либо запрет на создание заказа из-за текущего состояния всего приложения — это код 403. Или можно интерпретировать как ошибку в параметрах создания заказа (когда клиент передал не валидное значение цены товара, или номер версии корзины) — это 422.
Ещё можно было бы использовать статус "412 Precondition Failed", если придумать свой кастомный вариант условного заголовка, например:
If-Price-Match: product1=100$, product2=20$
Но это уже экзотика.
Насчёт 403 и аутентификации "средствами HTTP" — ваша правда. У меня глаза "замылились" описанием этой ошибки из русской википедии.
А то что никто не реализует 401 и аутентификацию как это написано в спеках — это опять же не вина самого HTTP (может мне конечно показалось, но я решил, что вы придерживаетесь мнения, что HTTP сам по себе бесполезен, и годится только как транспортный протокол).
Никто не запрещает вернуть эту информацию клиенту любым удобным способом. Но скорее всего будет лишним стандартизировать этот способ, поскольку для этого нет пока особых причин. Что с этой информацией будет делать прокси общего назначения, которая не знает особенностей конкретного приложения? Во многих случаях такая детализация не нужна даже клиентам.
Не хочу сейчас искать цитаты из дисера Филдинга, может я реально что-то не так понял. Но поверьте моему опыту — HTTP был сделан именно с такими идеями (а делался он по таким же принципам о которых писал Филдинг). Это очень хорошо видно, когда пытаешься работать с ресурсами через HTTP так как это в нём задумано — всё сразу становится на место, и нет ни каких не разрешимых затыков, и классификация ошибок оказывается вполне достаточной.
А если вы идёте "в чужой монастырь со своим уставом", то у вас и выходит что всё не так, и всё криво, и мало детализации и не понятно как использовать. В этом случае действительно надо использовать HTTP только как транспортный уровень и обходится только GET/POST-запросами (если действовать последовательно то надо только POST, но вы наверняка захотите кеширование сторонними решениями), и двумя кодами ответа: 200 — запрос обработан, 500 — запрос не обработан.
Вы противоречите сами себе. С одной стороны вы хотите "больше деталей", но при этом согласны с тем что никто не будет с этим заморачиваться. "Ленивые" разработчики даже не хотят использовать сделанную и описанную систему классификации ошибок в HTTP. И начинают выдумывать свою классификацию. Но винят в этом почему-то HTTP с его "недостаточно богатой системой ошибок".
Большинство претензий к статусам HTTP-ответов порождается узкостью мышления разработчиков. У разработчика ошибка в ТЗ называется "Нельзя создать пользователя с возрастом < 0". Разработчик смотрит в таблицу кодов HTTP и возмущается: "Как так, где ошибка 'XXX User is not born yet'? Говно ваш HTTP!".
Выдержка из википедии:
"HTTP — протокол прикладного уровня передачи данных, изначально — в виде гипертекстовых документов в формате HTML, в настоящее время используется для передачи произвольных данных."
Так что всё-таки прямое назначение HTTP — прикладное, а не транспортное. Иначе зачем там столько HTTP-методов с конкретной семантикой, различные статусы ответов, прикладные HTTP-заголовки? А в качестве транспортного уровня можно приспособить что угодно, хоть светодиод от жёсткого диска — зависит от вашей задачи и личных предпочтений.
Если бы вы немного внимательнее читали про REST, то знали бы что в нём на одном URL может находится одна сущность. И быстро бы поняли, что у вас что-то не так, если вы пытаетесь одним URL-ом обозначить несколько несколько сущностей. Как вы заметили — даже 404 не понятно как возвращать в данном случае. Я про это уже написал — если у вас ошибки не мапятся http-статусы, значит вы отклонились от ограничений REST и HTTP. В REST поверх HTTP есть один простой шаблон для операций над множеством сущностей. Это делают посредством дополнительной сущности, назовём её "выполнятор", с помощью которой создают сущность "задача". В рамках каждой задачи сервер может выполнять любые действия, которые не "влезают" в простые CRUD-операции с одной сущностью. В такой реализации отпадает проблема "как вернуть 404 если операция затрагивает больше одного объекта". В этом случае эта информация возвращается либо как ошибка 422 на запрос к "выполнятору", и в теле перечисляют список ресурсов, которые клиент передал неправильно. Или в теле сущности "задача", если отсутствие ресурсов было определено в момент её выполнения, а не в момент валидации параметров её создания.
Для быстрой классификации типа ошибки и какой-то её обработке промежуточными агентами (например кеширование) этого более чем достаточно. А все остальные подробности интересны (и будут понятны) исключительно клиенту. А клиент может себе позволить распарсить тело ответа и получить от туда информацию в любом формате, который удобен для конкретной системы.
Неужели Вы верите в возможность реализации стандарта, который сможет описать совершенно любые детали ошибок? Ещё и так что бы их смогли всегда понимать любые сторонние решения, которые работают с протоколом HTTP (прокси, мониторинги и т.п.)? Всегда найдётся кейс, которому будет не хватать этого супер-стандарта.
С 429 согласен, что можно придумать проксю, которая будет DOS-ить сервер за клиента. Правда клиент про это ничего не узнает и возможно даже отвалится по таймауту и сообщит пользователю, что сервер плохой и тормозной, вместо того что бы попросить его не посылать часто запросы.
Что делать проксе с 403 я не представляю. Сервер вполне конкретно дал понять: "Обрабатывать этот запрос не буду, потому что запрещено". Разве что прокся пошлёт запрос с другого IP адреса, если сервис использует авторизацию на основе диапазона IP адресов. Но это уже очевидно будет не прокся общего назначения, а какая-то специализированная, которая знает про специфические методы авторизации на сервере.
заказ уже был создан — зависит от ситуации. Например, если ресурс создаётся методом POST (т.е. клиент не знает URL нового ресурса, и просит другой ресурс выполнить его создание) я бы мог вернуть код 200 и в теле ответа уже существующий ресурс (у нас создание нового ресурса обозначается статусом 201 Created). Но это в случаях когда допустима "дедупликация". Тогда клиент понимает, что новый ресурс не был создан с нуля (обычно ему это и не важно) и сразу же получает данные по существующему ресурсу без дополнительных GET запросов. В этих случаях обычно клиенту всё равно до особенностей хранения данных на сервере — ему важно только, что бы на сервере был ресурс с требуемыми характеристиками.
На моей практике очень в редких случаях приходилось использовать 409. Судя по спецификации это применимо в кейсах, когда два запроса пытаются изменить один и тот же ресурс (т.е. они знают его URL), и при этом второй запрос посылает более старую версию ресурса, который создал/изменил первый запрос. Т.е. эта ошибка предполагает, что в запросе будет информация, по которой сервер сможет определить "версию" ресурса, и запретить перезаписывать имеющуюся у него более новую версию. Статус 409 сообщает клиенту, что он должен синхронизировать своё состояние с сервером, запросив с него новую версию ресурса (и он может это сделать, т.к. знает его URL).
заказ нельзя создать, потому что не разрешается иметь более N заказов одновременно — я бы перефразировал "заказ запрещено создавать" и вернул вполне очевидный в этом случае 403 с пояснением причины в теле ответа.
заказ нельзя создать, потому что товар в корзине закончился — выше я уже описал область применения кода 409. Тут он не очень подходит, т.к. скорее всего клиент не знает URL "заказа", а потому и не может быть конфликта с его изменением. Тут может подойти 403 — запрещено создавать заказ на основе пустой корзины. Или 422 — клиент передал недопустимый (устаревший или не существующий) ID-версии корзины (это так же решает проблему, когда клиент видит у себя 2 товара в корзине, а на сервере их уже 3 или это 2 других товара).
заказ нельзя создать, потому что цена товара изменилась — похоже на предыдущий пункт. Клиент не пытается изменить существующий заказ, что бы был "конфликт". Он просто послал не правильные, устаревшие данные. Очевидно, для реализации такого кейса, клиент должен посылать либо ID-версии товара, или пару [ID-товара, цена], что бы сервер смог сопоставить ожидания клиента с тем что есть у него. Я бы вернул 422 с указанием что или версия товара устарела, или его цена указана не правильно.
Если авторизация выполняется средствами HTTP и запрос "запрещён" именно из-за авторизации, то надо отвечать кодом 401. Код 403 из-за авторизации возвращают только тогда, когда авторизация делается не средствами HTTP. Т.е. не через заголовок Authorization или другие заголовки, URL или параметры в теле запроса. А например авторизация по IP-адресу или SSL сертификату.
Логировать важные ошибки непосредственно в самом приложении и настроить мониторинг этих логов, а не только смотреть на access-логи, например nginx-а, который стоит перед приложением. Или использовать access-логи, которые пишет само приложение — ему то ведь известны все детали и оно может добавить их в свои access-логи.
Ну а кейс с протухшими токенами я выше разобрал — если бы возвращали 401, то наверное было бы немного проще фильровать эти ошибки.
А каких действий вы ожидаете от абстрактной прокси, в ответ, например, на ошибку, когда клиент передал значение выходящее за пределы допустимого? Прокся что ли должна исправить это значение и повторно послать запрос серверу? Максимум чего бы я ожидал от адекватных промежуточных "агентов" в плане обработки ответов — это кэширование этих ответов. А для этого вполне достаточно той информации, которая передаётся в заголовках и описана в RFC. Не вижу пока причин придумывать что-то совершенно новое.
Если тут 404 заменить на 200, то ситуация кардинально не поменяется. Поэтому в той ситуации, которую автор "разгребал", в большей степени виноват не "дизайн" кодов, а сервис, который ошибочно возвращает не те коды.
Как говорится: "Если бы знал где упаду — соломку подложил". На все случаи жизни не придумать универсального решения. Если вам нужны специфические "графики" по какой-то частной проблеме, то вам придётся позаботься об этом самому. И для этого не обязательно выдумывать новые http-код. Гораздо проще в том месте где случилась ошибка, отправить подробности о ней в логи или в сервис типа Sentry, а не пытаться "угадать" ошибку по HTTP-ответу, предназначенному в первую очередь для клиента, и только во вторую — для мониторинга. Существующих кодов в HTTP вполне достаточно, что бы описать класс любой ошибки, которая может случится в системе построенной на принципах REST. Если же почему-то не получается "смапить" ошибку на имеющиеся коды, то очень вероятно, что вы отклонились от REST (или дополнительных ограничений HTTP, накладываемых поверх него). Я за несколько лет разработки так называемого RESTful API не встретил случая, когда не получилось подобрать стандартный HTTP-статус для классификации конкретной ситуации.
Любая качественно сделанная система будет дорогой в реализации (или её будут долго делать). В случае с правильным использованием HTTP и принципов REST сложность заключается, как мне кажется, в том что эти "штуки" не бьют моментально разработчиков по рукам, как, например, компиляторы языков программирования. Без опыта и знаний можно как в С играться с указателями, и всё будет даже работать. Косяки или вообще не заметят из-за незначительности системы. Или это случится сильно позднее, когда автор, вероятнее всего, уже уволится. И тогда такая система либо уйдёт на покой или за неё возьмутся дорогие специалисты и с матами будут исправлять.
Могу сказать, что лично мне "не дорого" использовать подход #3. Но это конечно будет лукавство, т.к. я относительно дорогой специалист. И у меня ушло несколько лет на то, что бы научится "варить" HTTP и REST по "заветам" Филдинга. У нас даже HATEOAS есть. И я на практике вижу плюсы всего этого.
Сомневаюсь что это практично. Как я уже написал выше — нет серебряной пули. Сложно будет придумать единое решение для более подробного описания конкретных ситуаций. Тех же HTTP-статусов довольно много. Плюс есть ещё различные стандартные HTTP-заголовки, которые дополняют статусы. В совокупности этого уже достаточно для большей части задач обработки запросов и ответов агентами находящимися в промежутке между сервером и клиентом. А уже на клиенте и сервере можно "договорится" и реализовать дополнительную детализацию так как удобно в конкретном случае. Хоть в теле запроса/ответа, хоть в заголовках.
PS: Не скажу что эта статья плоха. В ней есть доля правды и видны попытки разобраться. Хотя есть и чисто технические косяки в плане понимания предназначения конкретных статусов ответа и для чего они подходят, а для чего — нет. К сожалению в интернете не мало статей и заметок с такими и другими ляпами. И это совсем не улучшает понимание HTTP и REST среди разработчиков. Скорее даже наоборот — увеличивает предвзятое мнение, что всё это бесполезные знания, не применимые на практике, и надо просто всегда использовать, например, RPC через GET/POST и 2-3 HTTP-статуса для ответов.
Но фишка в том, что несмотря на всё, у HTTP есть заметный плюс — подробная и продуманная спецификация, которая уже не одно десятилетие применяется на практике. А у многих "протоколов мечты" либо нет спецификации, либо они не так сильно распространены.
Любой web-разработчик знает HTTP хотя бы минимально. И может легко найти спецификации и другие материалы про HTTP и разобраться как всё это можно использовать. Почти все знают что означают коды 200, 404 и 500. И это уже неплохо для начала — можно вести диалог на одном "языке".
Что-то у вас не сходится. 2 xor 2 равно 0, а не 4.
По себе могу судить, что меня больше коробит не то, что отказываются от буквы Ё как таковой, а то что её заменяют буковой E, полностью игнорируя то что Ё обозначает совсем другие сочетания звуков. Было бы меньше претензий если бы вместо Ё писали 'йо' или 'ьо' (после согласных). Но выбрали какое-то половинчатое решение — ни туда и ни сюда.
Судя по первому вашему упоминанию, от буквы Ё изначально отказались книгопечатники. И есть мысль, что это случилось не из-за каких-то академических причин, а из-за "чувства красоты" и банальной экономии (зачем делать две разные буквы, если можно одну).
Но меня (а я учил читать своих двоих детей) просто бесят тексты без буквы Ё. Кто-бы чего не говорил, но если человек не знает слово, в котором должна быть Ё, то он никогда его не прочитает правильно. Он всегда будет произносить Е. И конечно же есть "одинаковые" слова которые отличаются по смыслу только за счёт разницы между E и Ё.
Даже моя пожилая мама неправильно читает тексты с современными терминами, которые появились в последнее 10-15 лет. Приходится её поправлять, и говорить где нужна буква Ё.
В общем я считаю очень вредным отказ от буквы Ё в массовом книгопечатании. Хорошо хоть школьные учебники ещё не "пробили дно", и используют эту букву.
К сожалению GIL не избавляет от необходимости следить за параллельным доступом к вашим данным. GIL защищает от параллельного доступа исключительно внутренности самого интерпретатора. А данные вашей программы он защитить не может, т.к. не знает где и как они используются.
А как необходимость запускать блокирующую функцию в асинхронном окружении соотносится с вашей исходной задачей? Изначально вы поставили задачу — написать код один раз так, что бы его можно было использовать либо в асинхронном окружении, либо в синхронном. Тут совсем нет речи про то, что надо из асинхронного окружения вызывать какой-то блокирующий код. Это совсем другая задача.
В исходной задаче асинхронную версию библиотеки можно использовать в качестве основной и базовой реализации. А работу с ним из синхронного окружения либо переложить на пользователей (пускай сами явно используют asyncio.run() или loop.run_until_complete()). Или сделать какой-то декоратор, который ко всем вашим функциям и методам добавит синхронный враппер, использующий внутри себя asyncio.run(). Например такой враппер можно сохранять в свойство самой функции, что бы вызывать это как-то так:
PS: Стоит заметить, что asyncio.run(), вероятно, будет добавлять какой-то оверхед на создание евент-лупа и его удаление при каждом вызове ваших методов из синхронного окружения. Поэтому требуется тестирование. Как вариант можно попробовать реализовать однократное создание евент-лупа для вашей библиотеки с помощью какой-то функции для "настройки" работы библиотеки в синхронном окружении. И в дальнейшем использовать loop.run_until_complete().
Насчёт RESTfull я с вами согласен — это просто абстрактный термин, которым принято обозначать архитектуру приложений, якобы сделанную по принципам REST. Особой конкретики в этом нет, т.к. следуя принципам REST можно сделать разные архитектуры. По моему опыту 95% разработчиков, к сожалению, вообще не понимают REST, но при этом пытаются делать приложения и писать статьи про всё это. В результате интернет заполнен кодом и текстами не соответствующими принципам REST. И это только ещё больше запутывает, особенно новичков.
Но тема null-ов и отсутствующих полей не имеет отношения к REST как таковому. Просто есть 3 разных состояний поля в сущности: поле есть и имеет значение, оно null или его вообще нет. И эти состояния можно использовать в разных целях, в зависимости от контекста. Какие из них использовать и где — обычно понять не сложно, просто не забывая, что это действительно 3 разных состояния, которые лучше не смешивать. А потом задаться вопросами: какие из этих состояний допустимы, что каждое их них означает в конкретном случае. И если получается, что два разных состояния означают одно и то-же, то это повод хорошо подумать, действительно ли именно это вы хотели.
Про restfull вы несколько заблуждаетесь. Нет такого "правила" что возвращать надо то же что получил. Можно вообще получать в json, а отдавать в xml с другим набором полей. И в rest передают в обе стороны не сущности, а их некую репрезентацию. Какая при этом будет репрезентация — дело ваше, лишь бы её было достаточно для решения задач.
Но естественно часто просто удобнее если набор входных параметров является подмножеством результирующих свойств созданной сущности.
Возвращать только то что изменилось — это не удобно, нарушает схему ответа (разработчики клиентов вам точно спасибо не скажут), и накладно — если сущность была изменена другим клиентом, то придётся запрашивать её заново полностью, т.к. ваш запрос на изменение вернёт только то что он изменил.
Тут обязательно надо разделять "схему" входных параметров, и "схему" того что возвращает API (о чём говорится в статье). Входные параметры могут иметь более свободную форму. Например можно сделать так, что отсутствие в запросе поля или если оно равно null, будет преобразованно десериализатором-валидатором в "значение по умолчанию" (пустой массив).
А вот в схеме возвращаемого результата лучше избегать вольностей. Если поле должно быть всегда массивом — всегда возвращать массив, даже если по каким то причинам этого массива нет в хранилище данных.
Из моего опыта null нужен обычно только в тех случаях, когда он уместен в конкретной ситуации и не может быть иначе выражен конкретным типом данных. Например у типа datetime нет ни какого подходящего значения, которое бы можно было во всех ситуациях использовать в качестве "нет значения". Тут только null подходит. Для целых чисел иногда можно использовать 0, если бизнес-логика не допускает использование нуля в качестве нормального значения. С пустыми строками аналогично.
В случае коментов пустой массив это нормальная абстракция для "ещё нет коментов". Хотя бы потому что коменты надо куда-то добавлять. В null их не добавишь, а потому это не очень подходящее значение. Только если не использовать его как признак того, что коменты запрещены и их нельзя добавлять вообще.
Я лично стараюсь избегать использовать null без необходимости, т.к. он добавляет проверок в программе.
Что и как надо посылать при создании сущности — зависит от того нужено этоили нет. Если null допустимое значение для поля, то почему бы его не послать в POST запросе. Но можно так же сделать null как дефолтное значение поля при создании сущности. И тогда это поле можно вообще не посылать.
Это совершенно 3 разные ситуации:
Не надо пытаться экономить на спичках и интерпретировать одно через другое.
Пустая строка — это всё ещё строка, а не null и тем более не отсутствие поля. Тоже самое и с пустым массивом.
Null не тоже самое, что отсутствие поля. Например, в запросе на обновление сущности, передав в поле expires_time значение null мы сообщим что ресурс больше не имеет срока жизни. А если вообще не передать поле — значит его не надо менять, а оставить текущее значение.
А нельзя что-ли сначала к МКС потихоньку подключать новые модули с одновременным выводом из эксплуатации и "сбросом" в океан старых модулей, пока вся станция не обновится? Обязательно надо сначала всё старое разломать, и потом с нуля строить?