Все записи
6 мин

iOS Safari режет cookie на своём же поддомене: рабочий SameSite для веб-версии

вайбкодингкартараmvp

Если у вас фронт живёт на одном поддомене, а API на другом, ну, скажем, cartara.ru и api.cartara.ru, и вам надо, чтобы сессионная cookie спокойно ходила между ними, то скажу сразу, чтобы вы не потеряли полдня, как я: в iOS Safari ни SameSite=Lax, ни Strict не прилетят, как бы логично они ни звучали для поддоменов одного сайта. Реально работает на айфоне только одна связка, SameSite=None; Secure; HttpOnly; Domain=.cartara.ru; Path=/. И выяснил я это не из доки, а руками, прогнав живую матрицу браузеров через тестовый эндпоинт, и сейчас расскажу всю историю целиком, потому что на ней я пару раз успел подумать, что схожу с ума.

У Картары изначально весь вход шёл через Telegram, то есть Mini App и всё, что внутри мессенджера, авторизация там своя, никаких плясок с поддоменами. Но потом понадобилась веб-версия, чтобы человек мог зайти просто с компа в браузере, а не только из Телеграма, и сам вход на сайт я завязал на того же Telegram-бота через deep-link. И вот тут вылезает классика веба: фронт и бэк на разных поддоменах, а сессию надо держать общей. Серверную часть Claude Code накидал быстро, сессия подписывается через itsdangerous (HMAC-SHA256 с солью и TTL на 30 дней), а CSRF я закрыл Origin-check middleware по принципу fail-closed, то есть нет валидного Origin, значит запрос отбит, и никакого мягкого фоллбэка на Referer, потому что Referer браузеры режут на каждом шагу и доверять ему нельзя. На бумаге всё аккуратно, а потом ты открываешь сайт на телефоне, и cookie там просто нет.

Почему я вообще полез гонять матрицу руками

Можно было прочитать спеку про SameSite, кивнуть и поставить Lax, ведь поддомены это same-site, документация так и говорит, формально всё верно. Но я уже наелся ситуаций, когда теория про cookie и реальность на конкретном устройстве расходятся, и особенно это про Safari с его ITP (Intelligent Tracking Prevention), который ведёт себя не как все остальные браузеры. Так что вместо веры я сделал тестовый эндпоинт, который выставляет cookie с разными политиками, и стал заходить с него по очереди из Chrome на маке, из Safari на маке и из Safari на айфоне, и просто смотреть в лог, прилетела cookie обратно или нет.

Матрица получилась такая: Chrome 147 на macOS, Safari 17.6 на macOS и Safari 18.7 на iPhone, а варианты политики Lax, None плюс Secure, и Strict. Девять клеток, девять заходов, тупо и честно. И вот это слово, честно, тут ключевое, потому что когда вы спорите с Safari, единственный аргумент, который он принимает, это лог, где чёрным по белому видно, прилетела cookie или нет.

Что показал лог

В Chrome всё прилетало, как и ожидалось, никаких сюрпризов, любой вменяемый вариант работал. На Safari под маком тоже более-менее терпимо. А вот iPhone устроил мне сюрприз: и Lax, и Strict в iOS Safari cookie обратно не отдавали вообще, в логе ровно та строка, ради которой всё и затевалось, fail для текущего варианта, cookie не прилетела. То есть тот самый Lax, который по всем учебникам должен спокойно ходить между поддоменами одного сайта, на айфоне молчит. Заработал только один вариант, None плюс Secure, и то при условии, что добавлен Domain=.cartara.ru с точкой впереди, чтобы cookie шарилась на весь домен и его поддомены.

И вот тут самое неприятное для разработчика, у которого нет дома зоопарка устройств. Айфон у меня один, и я не могу проверить, как ведёт себя какой-нибудь другой айфон или другая версия iOS, я могу проверить только то, что лежит в руках. Как я сам себе сказал в процессе, это сафари, других нет у меня. Звучит как мем, но по сути это и есть реальность соло-разработки: ты не QA-отдел с фермой телефонов, ты один мужик, который тестит на том, что в кармане, и делает выводы по тому, что увидел своими глазами.

Почему iOS Safari вообще так делает

Если простыми словами, Apple через ITP очень агрессивно борется с трекингом, и cookie, которые ходят между разными хостами, ему подозрительны по умолчанию, даже если эти хосты твои собственные поддомены. Браузер не разбирается, что cartara.ru и api.cartara.ru это один проект и один владелец, он видит два разных хоста и применяет свою паранойю. Поэтому Lax, который во всех остальных браузерах означает примерно ходи в рамках сайта, тут перестаёт спасать, и приходится явно сказать браузеру, что это cross-site cookie, я знаю что делаю, через None; Secure. Secure обязателен, без HTTPS None просто не примут, но у нас и так везде TLS, так что это не проблема, а просто требование, которое надо не забыть.

Итоговую связку я зафиксировал и записал в отдельный файлик, чтобы через месяц не разгадывать это заново: SameSite=None; Secure; HttpOnly; Domain=.cartara.ru; Path=/. HttpOnly тут потому, что сессионному токену незачем светиться в JavaScript, его дело ходить между фронтом и API, а не валяться в document.cookie на радость любому скрипту. Кстати, тема приватности сессий в Картаре у меня уже один раз больно стрельнула, когда друг открыл приложение с моего телефона и увидел лишнего, так что к тому, кто и как держит сессию, я теперь отношусь с уважением.

Бонусом прилетел traefik

Когда серверная логика была готова, я полез деплоить веб-версию на инфру и наступил на вторую граблю, уже не про браузеры, а про оркестрацию. Инфра у Картары на k3s с traefik и Let's Encrypt, и traefik, как оказалось, забирал себе 80 и 443 порт через iptables, поэтому мой сервис банально не получал трафик так, как я ожидал. Лечилось это переездом на IngressRoute, ровно так же, как уже было сделано на dev-окружении, то есть готовый рабочий паттерн лежал рядом, надо было просто не выдумывать велосипед и повторить. Это, кстати, отдельный кайф соло-разработки через Claude Code: когда у тебя в одном проекте уже есть решённый кусок, ты говоришь сделай как на dev, и агент подтягивает тот же подход вместо того, чтобы изобретать новый способ упасть.

Что я из этого вынес

Первое и главное: документация про cookie это хорошая отправная точка, но не истина в последней инстанции, особенно когда в деле Safari и iOS. Никакая нейросеть, никакой Claude Code не угадает за вас, как поведёт себя ITP на конкретном айфоне, потому что это не вопрос логики кода, это вопрос поведения чужого браузера на чужом устройстве, и его надо именно проверить, а не вывести из первых принципов. Девять заходов через тестовый эндпоинт дали мне больше уверенности, чем три статьи на тему, и заняли меньше, чем я потом потратил на сам деплой.

Второе: для cross-subdomain авторизации в вебе сразу закладывайте SameSite=None; Secure; HttpOnly с Domain на корневой домен и точкой впереди, не тратьте время на Lax в надежде что прокатит, на айфоне не прокатит. Если делаете разработку saas и веб-сервисов под ключ или просто пилите веб-версию к своему Telegram Mini App, это сэкономит вам ровно те полдня, что я подарил Safari.

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