Пишите код так, как будто сопровождать его будет склонный к насилию психопат, который знает, где вы живёте.


Всем привет!


Я работаю тимлидом команды Integration Development в сервисе онлайн-бронирования отелей Ostrovok.ru и сегодня хотел бы поделиться своим опытом работы с различными API.



Как разработчик системы, работающей с внешними поставщиками, я часто встречаюсь с различными API – чаще всего это SOAP/REST или что-то на них похожее. Однако от работы со многими из них остается впечатление, что их писали, не руководствуясь ни техническими правилами, ни здравым смыслом – как будто бы по книге “Вредные советы” Григория Остера. В данной статье я постараюсь описать такие случаи в стиле “вредных советов” и рассмотрю примеры, связанные с XML. Комментарии и обсуждение приветствуются.


Историческая справка

SOAP (от англ. Simple Object Access Protocol – простой протокол доступа к объектам) – протокол обмена структурированными сообщениями в распределённой вычислительной среде. Первоначально SOAP предназначался в основном для реализации удалённого вызова процедур (RPC). Сейчас протокол используется для обмена произвольными сообщениями в формате XML, а не только для вызова процедур.


Переходим к примерам


1. Передача xml в url


Чего больше всего хотят пользователи API? Конечно же, простоты, надёжности и лаконичности. Так давайте не будем читать тело запроса, а будем принимать XML как url-encoded информацию как параметр пути запроса! Что может быть лучше:


http://exapmple.com/xml/form.jsp?RequestName%3DHotelRequest2%26XML%3D%3C%3Fxml%2Bversion%3D%221.0%22%2Bencoding%3D%22UTF-8%22%3F%3E%0A%3CHotelRequest2%2BBuyerId%3D%22test%22%2BUserId%3D%22test%22%2BPassword%3D%22test%22%2BLanguage%3D%22en%22%2BHotel%3D%22-100%22%2BProductCode%3D%221--%22%2BArrivalDate%3D%2223.12.2018%22%2BDepartureDate%3D%2224.12.2018%22%2BArrivalTime%3D%22%22%2BDepartureTime%3D%22%22%2BCurrency%3D%222%22%2BWhereToPay%3D%223%22%2BNumberOfGuests%3D%220%22%2BNumberOfExtraBedsAdult%3D%220%22%2BNumberOfExtraBedsChild%3D%220%22%2BNumberOfExtraBedsInfant%3D%220%22%2B%2F%3E

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


Спойлер

Я ума не приложу, почему так было сделано. Проблемы здесь следующие: у многих серверов есть ограничение на длину пути запроса, которое в них может пройти. Если XML будет большой по объёму данных, то можно вызвать ошибку 413 Entity Too Large как один из вариантов развития событий. Кроме того, увеличивается количество информации, так как мы производим url-encoding перед отправкой.


2. Передача информации путём избыточной вложенности объектов данных


Давайте подумаем, как бы сделать информацию в ответах как можно более сложнодоступной? Давайте использовать вложенные структуры, да ещё и в разных форматах! Сказано, сделано —


<Request>
    <InnerRequest>
        <RQ>[{"someInfo":"base64Data"}]
</RQ>
    </InnerRequest>
</Request>

Действительно, верхнеуровневый xml, внутри него ещё один xml, а внутри него json, в котором данные представлены в base64, а в нём снова json, и в нём уже будет нужная нам информация! Прекрасное решение, практически как из сказки про смерть Кощея, спрятанную в яйце.


Спойлер

Один из самых заметных минусов — это замедление работы парсинга ответа, пока все вложенные структуры будут пройдены, а после может оказаться, что код ошибки зашит в json, а не уровнями выше. Я понимаю, что кодирование бинарных данных в base64 внутри xml/json – это распространённая практика, но кодирование другого формата внутри другого формата – это уже за гранью добра и зла.


3. Добавление информации, не относящейся к данным запроса и не допустимой в рамках формата данных


Предположим, к нам в теле запроса приходит XML, мы обрабатываем его и даем ответ. Выглядит слишком сложно для хорошо продуманной и высоконагруженной системы. Давайте обязуем пользователей присылать тип данных в теле запроса. Как же это сделать? Прямо в теле запроса, конечно же.


XML=
<Request>
...
</Request>

Вот таким простым способом мы всегда будем знать, что нам пришёл запрос в формате XML.


Спойлер

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


4. Дублирование данных без необходимости


Предположим, у нас в структуре XML ответа есть очень-очень важная информация. Как показать это пользователю? Самое очевидное – давайте покажем её несколько раз в рамках ответа, тогда он точно обратит на неё внимание.


<Response>
    <Obj Info="Important">
        <ObjSetting Info="Important"/>
        <Name>SomeName</Name>
        <Info>Important</Info>
    </Obj>
</Response>

После этого конечный пользователь точно обратит внимание на поле Info.


Спойлер

В данном случае я задумался и даже спросил у компании, предоставляющей API, о смысле поля Info и о том, будет ли отличаться информация в тегах различного уровня. Ответ мне был: нет, не будет – они дублируют друг друга. Зачем вводить пользователей в заблуждение и делать ответ более тяжёлым, если в этом нет необходимости?


5. Передача параметров одного типа по отдельности, а не массивом


Во одном из API, которые мы используем для поиска отелей, есть поля, обозначающие возраст гостей. Какой формат представления лучше всего использовать для передачи этой информации? Пусть формат будет таким: каждый возраст будет отдельным обязательным тегом XML, и значение 0 мы будем считать, как отсутствие этого параметра.


<Request>
<Age1>20</Age>
<Age2>20</Age>
<Age3>0</Age>
<Age4>0</Age>
</Request>

Спойлер

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


6. Пересылка информации из предыдущих запросов в рамках цепочки вызовов API


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


<Request>
    <RequestInfo/>
    <PreviosResp>
    ...
    </PreviosResp>
</Request>

Спойлер

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


7. Неиспользование маркеров случившейся ошибки, таких как тег ошибки или http-код


Мы сделали наш прекрасный API и представили его миру. Но однажды что-то пошло не так, и мы не смогли сформировать ответ пользователю из-за внутренней ошибки. Что делать в таком случае? Просто дать шаблон ответа, без данных, без кодов ошибок или какой-либо другой информации. Никто не должен знать, что наш идеальный API может порой не работать!
Пример такого ответа:


<Response>
    <ImportantInfo/>
</Response>

– с кодом ответа 200 OK.


Спойлер

Замалчивание случившихся ошибок — очень плохая практика. Проблема в том, что всё выглядит так, как будто проблем в ответе нет: тега <Error> нет, http статус говорит, что всё в порядке. В этом случае нужно делать дополнительную валидацию полученной информации, чтобы не произошло непредвиденных последствий уже в нашей системе.


Заключение
Несмотря на большое количество документации по работе с технологиям SOAP/XML и проектированию API, многие проблемы всё ещё актуальны, и некоторые решения противоречат здравому смыслу. Надеюсь, этой статьей мне удастся обратить внимание разработчиков на не самые удачные подходы, чтобы уменьшить их количество в будущем.

Комментарии (8)


  1. Akuma
    27.03.2019 12:17

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

    Конечно, если человек сознательно так делает, то надо его приложить чем-нибудь, но все же зачастую это просто необходимость.


    1. EternalNomad Автор
      27.03.2019 13:55

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


    1. ashumkin
      29.03.2019 22:17

      А как вам:
      SOAP. Поле ответа Error. Цитирую разработчика "Если оно непустое(sic!), значит ошибки нет" WTF?! 8-O?!
      Я не знаю, что у человека было в голове (1С-ник, кстати), но "поймали" на этапе разработки протокола, и пресекли...


      1. Akuma
        29.03.2019 22:20

        Я тоже работал по SOAP с 1С-ником. И я тоже не знаю что там было в голове, не завидую тем, кто сейчас тот проект поддерживают :)


  1. kosmonaFFFt
    27.03.2019 12:44

    Если вы предоставляете WSDL для вашего API, то пусть все типы данных там будут any. Или object, я точно не помню.


    1. EternalNomad Автор
      27.03.2019 14:09

      Или строки. Были и такие, которые передовали логическую переменную строкой с цифрой. «1» — истина, и «0» — ложь. Через год использования,внезапно, стало приходить значение «2», но они быстро починили эту проблему.


  1. rraderio
    27.03.2019 13:30

    В данном случае я задумался и даже спросил у компании, предоставляющей API, о смысле поля Info и о том, будет ли отличаться информация в тегах различного уровня. Ответ мне был: нет, не будет – они дублируют друг друга. Зачем вводить пользователей в заблуждение и делать ответ более тяжёлым, если в этом нет необходимости?

    А если обьект `ObjSetting` используется еще где-то, а там это информация тоже нужна?


    1. EternalNomad Автор
      27.03.2019 14:05

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