• Что почитать на выходных №8

    Поздравляю со Старым Новым Годом! Мало ли, вдруг правда кто-то настолько безумен, чтобы его праздновать. Поехали:

  • Тринадцатая встреча

    Провели тринадцатую встречу.

    Максим Поправко рассказал о трудностях с нереляционными базами:

    Слайды

    Станислав Мехоношин выступил на тему деплоя и Докера:

    Слайды

    Ищем докладчиков на следующую встречу. Хотите выступить – пишите в личку или чат Телеграма.

  • Что почитать на праздниках №7

    Выкладываю чуть побольше, чтобы хватило на всю неделю.

    Осторожно, Невзоров!

  • Итоги 2016 года

    Подвожу итоги уходящего года: что сделал, что осталось в планах.

    1. Блог. За 2016 год я написал 96 постов (включая этот). Если в году 53 недели, получается почти по 2 поста в неделю. Неплохо держусь.

    2. Работа. Весь год я продолжаю работать на европейских заказчиков. На тему удаленной работы я писал большой пост. В Associated Press вышла статья с моим скромным участием. В середине лета случилось непредвиденное: попал в волну сокращений. Провел в поисках месяца три, весь извелся, зато нашел такое, что лучше не придумаешь. Заодно получил нехилый опыт поиска работы за рубежом, думаю отдельно написать.

      В середине лета снял клевый офис: светлый, одна из стен сплошное окно, диванчик, кофе-машина. Всегда рад гостям. Сидим впятером, думаем над расширением площади.

    3. Путешествия. Об увольнении я узнал будучи в Тунисе. Первый раз в жизни побывал на африканском континенте.

    4. Встречи. За год мы провели 12 встреч Глубокого Рефакторинга. Каждый месяц, без срывов, переносов. Сколько было митапов в вашей фирме? Один? Два? А у нас тринадцать.

    5. Волонтерство. Проводил слаконары для образовательного проекта Хекслет на тему удаленной работы и Емакса. Слаконар – это долгое выступление в Слаке на заданную тему с последующими вопросами читателей. По архивам Слаки я воссоздал два поста: про удаленную работу и про Емакс.

    6. Опен-сорц. Летом, в перерывах между поисками работы, написал библиотечку f для программирования в функциональном стиле на Питоне. Фактически это порт базовых возможностей Кложи. У библиотеки 100 звезд на Гитхабе и ей никто не пользуется. Мне было важно пройти весь путь от замысла до пакета на Pypi. Говоря иначе, просто довести дело до конца. Ссылки: исходники, статья, пакет.

    7. Гитхаб. Я стараюсь хранить данные в виде обычного текста, поэтому вынес всякое барахло на Гитхаб. В связи с этим график заметно позеленел:

      gh-chart

    8. ФП. Глубже увлекся функциональным программированием. Написал заметки о макросах, преимуществах Кложи, мультиметодах. Решил тестовое задание на Хаскеле.

    9. Английский. Первую половину года подтягивал английский, случая ролики с замедленным произношением. Написал пробный пост на английском языке.

    10. Книги. Прочитал далеко не все, что планировал: книги начинают скапливаться на полках. Осилил всего-то четыре: Atomic Scala, Цель, Бизнес как игра и ClojureScript Unraveled.

      Начал вести рубрику “Что почитать на выходных”. Идею стырил у Бирмана. Сделал 5 выпусков без сбоев, планирую продолжать.

      SICP так и остался недочитанным. Аналогично с альманахом Р. Душкина о программировании на Хаскеле. Надеюсь добить в 2017.

    Вот как я провел 2016 год. Спасибо всем, кто заходит на эту страничку. С праздником, увидимся в 2017 году!

  • Покупка авто

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

    Критерии обмана

    1. В объявлении указана заниженная цена, раза в 1.5-2 ниже рыночной. Продавец аргументирует тем, что срочно нужны деньги. Не верьте: хорошие машины, как правило, покупают очень быстро. Автомобилю в исправном состоянии не нужна такая скидка.

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

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

      Возможно, когда продавец говорит, что у него много покупателей, он действительно в это верит! Охладите его пыл: из ста позвонивших назначат встечу пять, из этих пяти лишь один дойдет до этапа диагностики, и по ее результатам, он, ВОЗМОЖНО, подумает, покупать или нет.

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

    4. Продавец интересуется, где вы находитесь. Это развод: после вашего ответа он называет отдаленное место, откуда трудно добираться, и просит перевести на карту тысячу на бензин.

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

    6. Продавец находится не в вашем городе. Междугородная покупка повышает все риски. Это траты на дорогу, время, нервы. В чужом городе нет прежних связей и контактов, чтобы обратиться за помощью к друзьям или знакомым. Часто человек настолько измотан дорогой, что покупает не глядя. Бывает, сам видит, что машина не стоила предприятия, но покупает, потому что жалко потраченных сил.

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

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

    8. При звонке продавец уточняет, насчет какой машины вы звоните. Этим грешат агенты и перекупы, у которых по 3-4 авто в работе. Логично, что частник, у которого одна машина, не будет уточнять марку. По этой же причине вы не называете ее, когда звоните. Просто “я звоню по объявлению”.

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

    Порядок покупки

    1. Позвоните и расспросите все об автомобиле. Не соглашайтесь на отговорки “в объявлении все написано”. Хороший частник всегда любит почесать языком о собственном авто. В разговоре часто проскакивают детали, которых не было в объявлении: царапина здесь, скол там. Вы не перебиваете, пусть говорит пока не устанет. Бывало, что из откровенного разговора становилось ясно, что машина – не мой вариант, даже если объявление смотрелось идеально.

    2. Если из разговора все норм, попросите скинуть скан ПТС. Желательно должен быть один владелец не считая автосалона. ПТС не должен быть копией. Да, всякое бывает: потерял, промочил, жена постирала. Но чаще это означает прохладные схемы с автомобилем.

      ВИН-номер должен быть четко виден. Этот номер вы пробиваете на сайте ГИБДД. Не должно быть ДТП, автокредитов, угонов, розысков. В нижней части страницы находятся ссылки на страховые компании, где по номеру автомобиля можно пробить схожие данные: кредиты и тд.

    3. Если ПТС и ГИБДД не выявили проблем, договаривайтесь о встрече сначала просто посмотреть, а потом в диагностическом центре. Это не должно быть тем местом, которое предлагает продавец: у него могут быть связи. Некоторые думают, что диагностика – это обязательно загон на яму. Вовсе не обязательно: достаточно много подвохов можно выявить внимательным осмотром.

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

      Все это должен делать, конечно, специально обученный человек, а не вы. Но ближе к концу я тоже стал кое-что понимать.

    4. Если и первичный осмотр не выявил проблем, садите специалиста в машину и едете 200-300 метров. Не должно ничего стучать. Если после проезда нет претензий, ставить авто на яму уже не обязательно: серьезные трудности должны были быть выявлены заранее. Но можно и загнать, если есть время.

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

    6. Неопытный продавец может думать, что диагностика – это быстро. Предупредите, что потребуется 2-3 часа его времени, так что лучше отпроситься с работы. Так, с одним продавцом пришлось расстаться, так как он тупо ныл, что не успевает.

    7. Если и диагностика прошла ок, оформляете на нейтральной территории три договора купли-продажи авто. Это не должна быть фирма-однодневка или приятели продавца. Это не должен быть какой-то левый бланк, стыренный из интернета. Договор стоит 1 т.р, расходы пополам. В моем случае договор составили прямо на диагностике. В договорах ставят печати, но расписываться нигде не нужно! Сделаете это в момент передачи денег.

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

    Ну вот вы и купили авто, поздравляю! Дальнейшие шаги типа регистрации в ГАИ описывать не буду, потому что статья скорее о покупке и всему, что ей предшествует.

    Хоть я неопытный водитель и брал подержанную машину, описанное выше пригодится и при покупке нового авто. Потому что никто не застрахован от случайностей. Директор фирмы, где я проходил диагностику, рассказывал, как осматривал новую машину. Болты на боковой стойке были скручены. Оказалось, при разгрузке с поезда помяли дверь и оперативно поменяли на новую с выставочного образца. Так что в салон тоже нужно брать обученного человека.

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

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

    С наступающим!

  • Мультиметоды в Кложе

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

    Не скажу, чтобы это плохо: язык должен быть простым. Чем меньше правил и бест-практик нужно помнить, тем легче на нем писать. Однако, есть в Кложе классные штуки, точечное применение которых сэкономит время и объем кода.

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

    Мы знаем, что ООП базируется на инкапсуляции, наследовании и полиморфизме. Рассмотрим последний. Полиморфизм – это когда у метода может быть несколько реализаций. Конкретная реализация выбирается в зависимости от типов параметров.

    В классическом ООП нам бы привели такой пример. У класса Geometry есть метод square для вычисления фигур. На вход могут подать окружность, квадрат и треугольник. Запишу на каком-то выдуманном языке:

    class Geometry:
    
        real square(Circle c):
            return Math.PI * c.radius * c.radius;
    
        real square(Rectangle rect):
            return rect.a * rect.b;
    
        real square(Triangle tri):
            real p = (tri.a + tri.b + tri.c) / 2;
            return Math.sqrt(p * (p - tri.a) * (p - tri.b) * (p - tri.c))
    

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

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

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

    Рассмотрим Хаскель. То, что выше я записал на выдуманном языке, в Хаскеле будет так:

    data Circle = Circle Float
    data Rectangle = Rectangle Float Float
    data Triangle = Triangle Float Float Float
    
    square :: Circle -> Float
    square (Circle r) = 3.1415 * r * r
    
    square :: Rectangle -> Float
    square (Rectangle a b) = a * b
    
    square :: Triangle -> Float
    square (Triangle a b c) = sqrt $ p * (p - a) * (p - b) * (p - c)
    where
      p = (a + b + c) / 2
    

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

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

    (defmethod collide-with ((x asteroid) (y asteroid))
       ;; deal with asteroid hitting asteroid
       )
     (defmethod collide-with ((x asteroid) (y spaceship))
       ;; deal with asteroid hitting spaceship
       )
     (defmethod collide-with ((x spaceship) (y asteroid))
       ;; deal with spaceship hitting asteroid
       )
     (defmethod collide-with ((x spaceship) (y spaceship))
       ;; deal with spaceship hitting spaceship
       )
    

    Особенность примера с Лиспом в том, что второй параметр в каждой паре ведет себя как предикат. В данном случае тип spaceship срабатывает как проверка того, что x – экземпляр космического корабля. Однако, вместо spaceship можно передать другие проверки: integer?, string?, even?, словом, любой унарный предикат.

    Столь гибкая система мультиметодов позволяет определить разное поведение для одного и того же типа, но разных значений. Например, для отрицательной суммы денег одно поведение, для нуля – второе, для положительной – третье. Для даты: выходной или нет, високосный год или нет и т.д.

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

    В Кложе более гибкая система мультиметодов. В рассмотренных выше примерах мы не могли изменить сам принцип подбора метода, или диспатча. В Кложе, наоборот, вы обязаны определить диспатч! Рассмотрим пример:

    (defmulti foo class)
    

    Данное определение говорит: прежде чем искать реализацию, метод получит класс аргумента.

    Следующее определение расширяет мульти-метод реализацией для Long: если передано длинное целое, получим строку "an integer".

    > (defmethod foo Long [x] "an integer")
    #multifn[foo 0x478c7d41]
    > (foo 42)
    "an integer"
    

    Добавим для строки:

    > (defmethod foo String [x] (format "%s is a string" x))
    #multifn[foo 0x478c7d41]
    > (foo "test")
    "test is a string"
    

    Если ни одно соответствие не подошло, будет ошибка:

    > (foo nil)
    IllegalArgumentException No method in multimethod 'foo' for dispatch value: null
    clojure.lang.MultiFn.getFn (MultiFn.java:156)
    

    Не страшно, добавим реализацию по умолчанию:

    > (defmethod foo :default [x] (format "you passed %s" x))
    > (foo nil)
    "you passed null"
    

    Диспатч может работать самым разным способом, например, проверять на типы все аргументы. Повторим пример с площадью фигур:

    (defmulti square (fn [& args] (mapv class args)))
    
    ;; rectangle
    (defmethod square [Long Long] [a b] (* a b))
    (square 2 3) ;; 6
    
    ;; circle
    (defmethod square [Double] [r] (* Math/PI r r))
    (square 1.1) ;; 3.8013271108436504
    
    ;; triangle
    (defmethod square [Long Long Long]
                   [a b c]
                   (let [p (/ (+ a b c) 2)]
                     (Math/sqrt (* p (- p a) (- p b) (- p c)))))
    (square 2 2 2) ;; 1.7320508075688772
    

    Замечу, что на самом деле Кложа проверяет значения диспатча и образца не простым сравнением, а функцией isa?, что подразумевает иерархию. Так, чтобы проверку проходили типы PersistentArrayMap и PersistentHashMap, достаточно указать базовый класс clojure.lang.APersistentMap.

    Наконец, проверять можно не только на типы, а банально на значения, диапазоны, предикаты и тд. В текущем проекте я проверяю последний переданный аргумент. Это может быть как айдишка, так и запись БД. В зависимости от этого условия срабатывает разная логика.

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

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

    Напомню ситуацию с примерами на Джаве и Хаскеле выше. Пусть фукция square и класс Geometry находятся в чужих библиотеах. Тогда вы никак не сможете изменить их! В лучшем случае вы унаследуете класс Geometry, добавите свой метод для ромба. Но если есть еще одна библиотека, которая работает с Geometry, вы никак не сможете на это повлиять.

    Напротив, идея расширения чужих определений работает в Кложе просто убийственно. Я нигде не видел ничего подобного.

    В Кложе мультиметоды используют когда трудно предугадать, какие типы данных будут поступать на вход. Это прекрасное решение для абстракций вроде работы с БД, парсингом данных, декораторов и тд.

    Мультиметоды в Лиспах – мощнейшей инструмент, порой в корне меняющий принцип мышления и разработки.

  • Что почитать на выходных №6

    С опозданием, но все же:

    И конечно:

    «Афиша Daily» поговорила с людьми, которые считают себя зависимыми от порнографии, и расспросила психотерапевтов о том, как порно влияет на нашу жизнь и как контролировать это влияние.

  • Руководство по кросс-доменным запросам (CORS)

    На прошлой неделе я внедрял в проект CORS-запросы – современный способ кросс-доменного Аякса. По следам прочитанной документации и набитых шишек подготовил небольшой мануал. Это вольный пересказ англоязычных статей, вопросов со Стека и скромный личный опыт.

    Но сначала короткая предыстория. Я неравнодушен к собеседованиям, как вы, наверное, знаете. Чаще я собеседовал бекенд, т.к. к фронтенду отношусь прохладно. Но тема фронтенда обладает огромным потенциалом для оценки уровня кандидата. Я считаю, что разработчик должен в деталях понимать протокол HTTP и тонкости работы браузера.

    Один из вопросов на тему фронтенда звучит банально: как на клиенте получить данные с другого домена?

    Некоторые кандидаты отвечают, что проблемы нет, достаточно выполнить Аякс-запрос. И с большим удивлением узнают, что, оказывается, нельзя: сработают какие-то там политики безопасности.

    Один собеседуемый заявил, что в Фаерфоксе это работает, достаточно зайти на страницу для разработчиков и что-то там переключить. Я не против такого ответа. Следующий вопрос, как вы заставите всех пользователей установить именно Фаерфокс и поменять системный флаг?

    С понятием JSONP вообще беда – никто не может объяснить, как это устроено. Разработчики думают, что это обычный Аякс, только с каким-то P на конце, то ли баг, то ли фича. А это вообще ни разу не Аякс.

    Аббревиатура CORS появилась недавно, и спрашивать о ней нет смысла. Этот пробел восполняет данный мануал.

    Разберем вопросы из предыдущих абзацев. Действительно, слать Аякс-запросы к серверам с другим доменом запрещено на уровне браузера. Однако, в интернете полно сайтов, где значимая часть контента подгружается со сторонних серверов. Например, этот блог работает на статичном генераторе Jekyll, в котором нет комментариев. Делиться мнениями помогает сервис Discuss: лента комментариев встраивается Джаваскриптом. Получая и отправляя комментарии, вы взаимодействуете с серверами Discuss, а мой блог вообще ни при чем. Значит, слать запросы Аяксом все же можно?

    Нет, здесь работает JSONP. Аббревиатура значит JSON with Padding (с подкладкой). Идея основана на лазейке в стандартах: загружать скрипты с других доменов не запрещено! Скажем, если в файле example.js на чужом сервере написано что-то вроде:

    alert("hello!");
    

    , то достаточно подгрузить его тегом <script> на страницу:

    <script src="http://example.com/static/example.js"></script>
    

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

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

    <script
    src="http://example.com/api.js?method=get_user&user_id=42&callback=processUser"
    ></script>
    

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

    {name: "Ivan", age: 30}
    

    , то на клиенте ничего не произойдет: объект просто считается в память. Именно поэтому передают третий параметр callback – имя функции, объявленной на клиенте, в которую нужно завернуть пользователя. С этим параметром ответ станет другим:

    processUser({
        name: "Ivan",
        age: 30
    });
    

    На клиенте отработает код, зашитый в функцию processUser: вывести данные в консоль, отрисовать виджет и т.д. Вот как работает JSONP.

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

    Далее, подгрузка скриптов ни разу не безопасней, чем Аякс. Целое семейство вирусов занимается тем, что добавляет на страницу браузера скрипты для отрисовки баннеров порно и казино. Когда вы подключаетесь к интернету через мобильных операторов, обсосы вставляют в HTML-трафик скрипты для отрисовки виджетов (если соединение не HTTPS).

    JSONP работает только методом GET, что сводит на нет возможности REST-интерфейса. Для REST-сервисов приходится писать прокладки-прокси, т.е. множить костыли.

    Добавив скрипт на страницу, в дальнейшем вы не можете отследить его судьбу. Если у Аякс-запроса есть специальные коллбеки для основных событий (начало, удачное завершение, таймаут, неудачное завершение), то у скрипта ничего такого нет. Загрузился ли он? Ответил ли сервер? Была ли ошибка? Никто не знает.

    Ясно, что в 2016 году приложениям на js нужен надежный способ забирать данные с серверов. Чтобы это была законно, а не по-воровски в обход протоколов и стандартов. Таким способом стал CORSCross-Origin Resource Sharing, кросс-доменные запросы.

    Идея проста – пусть клиент шлет Аякс-запрос к чужому серверу. Браузер добавит в запрос особые заголовки с информацией о том, что запрос с другого домена. На их основании сервер решит, как обрабатывать такой запрос, и добавит особые заголовки в ответ. Удобно, правда?

    Техническая реализация несколько сложнее. Стандарт CORS различает “простые” и “сложные” запросы. Простым считается запрос методами:

    • HEAD
    • GET
    • POST

    и заголовками:

    • Accept
    • Accept-Language
    • Content-Language
    • Last-Event-ID
    • Content-Type, но только со значениями:
      • application/x-www-form-urlencoded
      • multipart/form-data
      • text/plain

    Если ваш запрос удовлетворяет этим критериям, можно слать Аякс к другому домену из любого современного браузера. При этом браузер добавит заголовок Origin с адресом страницы, откуда инициирован запрос. Подделать заголовок скриптом не удастся.

    Сервер, получив на обработку подобный запрос, должен прочесть Origin и решить, как его обрабатывать. Заголовок ответа Access-Control-Allow-Origin регулирует, с какого домена разрешено запрашивать данные. Это может быть как веб-адрес, так и знак астерикса (звездочки), если разрешено всем. Несколько разных адресов через запятую, к сожалению, не поддерживаются.

    Пример CORS-запроса:

    POST /foo/bar HTTP/1.1
    Origin: http://foreign.com
    Host: test.com
    

    и ответа с разрешением на получение данных:

    200 OK HTTP/1.1
    Access-Control-Allow-Origin: http://foreign.com
    Content-Type: text/html; charset=utf-8
    
    <h1>Welldone</h1>
    

    Обратите внимание на такую вещь: мы намерены использовать CORS, чтобы дергать чужие API. С вероятностью почти 100% они работают по протоколу JSON, то есть принимают и отдают заголовок Content-Type: application/json. Вроде бы мелочь, но такой запрос автоматом перестает быть простым и переходит в разряд “сложных”, где схема взаимодействия иная.

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

    При этом браузер не дурак и все запомнит: если разрешили только методы GET и POST, то PUT и DELETE не сработают. Аналогично с заголовками: если помимо стандартных разрешено использовать только Authorization, то нужно передать его и ничего другого.

    Пример сложного запроса:

    OPTIONS /cors HTTP/1.1
    Origin: http://api.bob.com
    Access-Control-Request-Method: PUT
    Access-Control-Request-Headers: X-Custom-Header
    Host: api.alice.com
    Accept-Language: en-US
    Connection: keep-alive
    User-Agent: Mozilla/5.0...
    

    Клиент хотел отправить Аяксом запрос методом PUT на урл http://api.alice.com/cors с сайта http://api.bob.com. Поскольку это сложный запрос, браузер запросил разрешение: типа, хочу сделать PUT на этот урл с особым заголовком X-Custom-Header. Сервер ему на это:

    200 OK HTTP/1.1
    Access-Control-Allow-Origin: http://api.bob.com
    Access-Control-Allow-Methods: GET, POST, PUT
    Access-Control-Allow-Headers: X-Custom-Header
    Content-Type: text/html; charset=utf-8
    

    Или иными слвами: разрешено ходить методами GET, POST, PUT и с заголовком X-Custom-Header. Это подходит под критерии первоначального запроса. Браузер делает второй запрос куда мы намеревались вначале.

    Первая стадия, когда делается запрос OPTION, официально называется preflight request. Надо сказать, такое взаимодействие весьма прозрачно отражается в браузере. Например, в консоли разработчика в Хроме видны оба запроса со всеми заголовками.

    Вот такие строгости. В нашем проекте API на стороне сервера требует заголовки Version (версия операции), Authorization (авторизация по токену) и Content-Type (JSON), поэтому в ответе указываем

    Access-Control-Allow-Headers: Version, Authorization, Content-Type
    

    , иначе запрос не пройдет.

    Теперь все это можно протестировать. Запускаем локальный сервер, открываем консоль Хрома и пишем:

    var xhr = new XMLHttpRequest();
    xhr.open("GET", "http://127.0.0.1:5000/api/users", true);
    xhr.setRequestHeader("authorization", "Token xxxxxx");
    xhr.setRequestHeader("Version", "1");
    xhr.send();
    xhr.responseText
    >> "{"users":[{name "Ivan"...
    

    Забавно, что заголовок Origin в этом случае будет равен https://google.com, потому что Хром считает пустой страницей главную Гугла.

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

    Несмотря на кажущуюся простоту, реализовать поддержку CORS на сервере требует времени. Первоначально я хотел использовать чужую библиотеку, но после 10 минут чтения исходного кода понял, что автор НЕПРАВИЛЬНО понял спецификацию и реализовал ее с ошибками. Лишний раз убедился, авторы сторонних библиотек – не боги, а такие же смертные. Они могут тупить, ошибаться. Лучше потратить день на чтение спеки и сделать все правильно, чем доверять первому встречному решению.

    Расскажу теперь о тонкостях, с которыми столкнулся при внедрении CORS в проекте. Прежде всего, чтобы сократить число preflight-запросов, стоит либо кешировать эндпоинт OPTIONS заголовками:

    Cache-Control: no-cache, must-revalidate
    

    , либо вообще объявить его на уровне Nginx:

    location / {
         if ($request_method = 'OPTIONS') {
            add_header 'Access-Control-Allow-Origin' '*';
            add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
            add_header 'Access-Control-Allow-Headers' 'Version, Authorization, Content-Type';
         }
    

    Еще одна тонкость: даже если возвращаете ответ с не-положительным статусом, например, не прошла валидация или нет прав, заголовок Access-Control-Allow-Origin обязан присутствовать. Если заголовка нет, браузер решит, что CORS-запрос запрещен и не прочитает ответ.

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

    Ссылки:

    • Using CORS

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

    • Enable CORS

      Сайт, целиком посвященный CORS: описание, статьи, конфиги, примеры кода.

  • Что почитать на выходных №5

    Поехали, техническое:

    • MVC vs. OOP

      Замечания Егора Бугаенко об известных паттернах.

    • What’s Wrong With Object-Oriented Programming?

      У того же автора: подборка цитат известных программистов о том, что ООП отстой.

    • Using CORS

      Отличный мануал как прикрутить кросс-доменные запросы к серверу и клиенту. Намереваюсь написать свой туториал по следам недавних изысканий.

    На сладкое:

  • Мой вклад в clj-http

    В проект clj-http приняли мой скромный пулл-реквест. Теперь, если запрос был вызван с флагом :throw-exceptions true и вернулся плохой ответ (статус не 2хх, 3хх), то во вбрасываемый словарь добавляеся ключ :type :clj-http.client/unexceptional-status. Это на ура работает с библиотекой Slingshot, потому что отлов исключений по полю :type стал негласным стандартом.

    Если абзац выше звучит для вас бессмыслецей, расскажу подробней. Изначально в Кложе идут унылые Java-исключения, которые плохо ложаться на мир структур и коллекций. Но поскольку Кложа это Лисп, а в Лиспе любая проблема решается его же силами, умельцы написали библиотеку для альтернативных исключений slingshot.

    Библиотека упрощает работу с исключениями до мыслимого предела. Теперь можно вбрасывать не только объект, унаследованный от Throwable, а вообще что угодно. Лучше всего подходит словарь. Форма отлова исключения, выброшенного из slingshot, может иметь разную структуру. Например, предикат, но удобней и короче будет вектор двух элементов, где первый – ключ, а второй – значение.

    Если раньше приходилось передавать неочевидный предикат, который проверял, что в словаре есть поле status, и оно числовое:

    (catch
      (fn [response]
        (-> response :statsus integer?))
      ...)
    

    , то теперь достаточно сравнить так:

    (catch [:type :clj-http.client/unexceptional-status])
    

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

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

Страница 1 из 27