2САЙТ

Белый экран в Telegram Mini App: почему приложение не открывается на iPhone и ПК и как это починить

Запустили Telegram Mini App, а у пользователей на iPhone белый экран, на ПК выбрасывает в браузер или падает ошибка initData? Разбираем 6 фатальных инженерных ошибок TMA: от заголовков CSP и строгой песочницы WebKit до проверки хэша HMAC и багов BotFather.

Белый экран в Telegram Mini App: почему приложение не открывается на iPhone и ПК и как это починить
09.09.2026
📱 TELEGRAM MINI APPS🛠️ ИНЖЕНЕРНЫЙ АУДИТWEBVIEWОШИБКИ РАЗРАБОТКИ

Почему в браузере всё работает, а в Telegram — белый лист?

Типичная история из практики разработки 2026 года: команда создаёт Telegram Mini App на современном стеке (React, Next.js, Vue или Svelte), запускает локальный сервер и тестирует интерфейс в десктопном Chrome с открытым эмулятором мобильных устройств (Device Toolbar). В браузере всё летает: анимации плавные, компоненты монтируются моментально, API возвращает ответы за 40–50 миллисекунд.

Затем проект выкатывают на тестовый стенд, подключают к боту через BotFather и запускают первый поток пользователей или рекламу. И тут начинается лавина жалоб:

  • До 30–40% владельцев iPhone при нажатии на кнопку видят чистый белый экран. Приложение даже не начинает показывать индикатор загрузки (скелетон или спиннер).
  • Пользователи десктопного клиента Telegram Desktop или браузерных версий (Web K и Web A) получают пустое серое окно, либо их выбрасывает во внешний браузер, где интерфейс полностью ломается.
  • Пользователи, свернувшие мессенджер на полчаса и вернувшиеся обратно, натыкаются на бесконечный прелоадер или ошибки 401 Unauthorized.

Причина кроется в фундаментальном заблуждении: Telegram Mini App — это не «обычный адаптивный сайт, открытый внутри мессенджера». Это гибридное веб-приложение, запущенное в изолированном системном контейнере WebView, управляемом мостом Telegram WebApp SDK.

При этом среда исполнения на разных платформах кардинально отличается:

  • iOS (iPhone и iPad): приложение рендерится через движок Apple WebKit (WKWebView) со строгой политикой защиты конфиденциальности (Intelligent Tracking Prevention), специфическим поведением песочницы для cookie и виртуальной экранной клавиатуры.
  • Android: используется системный компонент Android System WebView на базе Chromium со своими ограничениями кэширования и разрешений.
  • Telegram Desktop и Telegram Web: приложение загружается внутри изолированного HTML-элемента <iframe>, подчиняющегося жестким правилам безопасности междоменного встраивания.

Ниже мы подробно разберём 6 фатальных инженерных ошибок, из-за которых Telegram Mini App падает в белый экран, и покажем проверенные решения для каждой из них.

Сводная матрица: где и почему падает Mini App

В таблице ниже собраны основные симптомы сбоев, уязвимые платформы и быстрые способы диагностики:

ОшибкаПлатформа сбояВнешний симптомПервопричинаСпособ устранения
Заголовки CSP и X-Frame-OptionsTelegram Desktop, Web (K / A)Белый прямоугольник, ошибки в консолиX-Frame-Options: SAMEORIGIN или DENYДиректива frame-ancestors https://*.telegram.org
Краш localStorage в WebKitiOS (iPhone, iPad)Белый экран до отрисовки первого кадраИсключение SecurityError: The operation is insecureSafe-storage полифилл с in-memory fallback и CloudStorage
Сбои валидации initDataВсе платформыОшибка 401 Unauthorized, вечный лоадерНеверный HMAC-SHA256, сбой сортировки, протухание датыЭталонная валидация подписи на бэкенде с допуском по времени
Сдвиг вьюпорта клавиатуройiOS (iPhone)Контент улетает вверх, кнопка вне экранаСпецифика ресайза WebKit при скрытии клавиатурыВызов expand(), обработка viewportChanged, --tg-viewport-height
Неверная ссылка в BotFatherDesktop, MobileВыброс во внешний браузер вместо WebAppОбычный URL вместо объекта WebAppInfoНастройка Web App кнопки в BotFather и inline-клавиатуре
Неполная цепочка SSL (CA)iOS (Safari WebKit)Белый экран без единого сетевого запросаОтсутствие промежуточного сертификата в NginxУстановка полного бандла сертификатов fullchain.pem

Ошибка №1. Заголовки CSP и X-Frame-Options: когда браузер молча блокирует iframe

В десктопном приложении (Telegram Desktop для Windows и macOS) и в браузерных клиентах (web.telegram.org) Mini App открывается внутри стандартного HTML-фрейма <iframe>.

Большинство современных веб-серверов (Nginx, Caddy, Apache), облачных прокси (Cloudflare) и фреймворков (Next.js, Remix, Nuxt) по умолчанию настроены на максимальную безопасность и отдают заголовки, предотвращающие атаки кликджекинга (Clickjacking):

  • X-Frame-Options: DENY или X-Frame-Options: SAMEORIGIN
  • Content-Security-Policy: frame-ancestors 'none' или frame-ancestors 'self'

Как только браузер видит такой заголовок в ответе страницы, загружаемой внутри <iframe>, он мгновенно и безусловно блокирует рендеринг документа. В консоли десктопного браузера появляется красная ошибка Refused to display in a frame because it set 'X-Frame-Options' to 'sameorigin', а пользователь видит абсолютно чистый белый прямоугольник.

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

Как починить

Заголовок X-Frame-Options является устаревшим стандартом: он не поддерживает указание нескольких разрешённых доменов. Современный стандарт W3C требует использования директивы frame-ancestors в заголовке Content-Security-Policy.

Для веб-сервера Nginx конфигурация виртуального хоста Mini App должна выглядеть следующим образом:

nginx
server {
    server_name app.vash-domen.ru;

    # Отключаем устаревший заголовок X-Frame-Options, если он выставлен бэкендом
    proxy_hide_header X-Frame-Options;

    # Разрешаем встраивание во фрейм только нашему сайту и клиентам Telegram
    add_header Content-Security-Policy "frame-ancestors 'self' https://web.telegram.org https://*.telegram.org https://telegram.org;" always;

    # Базовые заголовки защиты от подмены типов контента
    add_header X-Content-Type-Options "nosniff" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Если ваше приложение работает на Next.js без проксирующего веб-сервера, скорректируйте заголовки в файле next.config.js:

javascript
// next.config.js
module.exports = {
  async headers() {
    return [
      {
        source: '/:path*',
        headers: [
          {
            key: 'Content-Security-Policy',
            value: "frame-ancestors 'self' https://web.telegram.org https://*.telegram.org https://telegram.org;",
          },
        ],
      },
    ];
  },
};

Ошибка №2. Строгая песочница Apple WebKit и внезапный краш localStorage на iPhone

Это самая частая и коварная причина «белого экрана на iPhone», на которую приходится до 70% всех инцидентов в мобильном трафике.

В операционной системе iOS браузерный движок WebKit (WKWebView) работает в режиме повышенной изоляции персональных данных. Если у пользователя в настройках устройства включена опция «Блокировка всех cookie» (Block All Cookies), активирован приватный режим (Private Browsing) или сработал алгоритм защиты от отслеживания Intelligent Tracking Prevention (ITP), WebKit полностью блокирует доступ к синхронному веб-хранилищу:

  • window.localStorage
  • window.sessionStorage

При любой попытке прочитать или записать значение:

javascript
const token = localStorage.getItem('auth_token');

WebKit не просто возвращает null — он выбрасывает фатальное исключение уровня браузера: DOMException: The operation is insecure. (или SecurityError: The operation is insecure).

Почему приложение падает намертво

В 9 из 10 современных одностраничных приложений (SPA) популярные менеджеры состояния (Zustand с мидлваром persist, Redux-Persist, Pinia Persistedstate) вызывают обращение к localStorage синхронно на этапе импорта и инициализации JavaScript-модулей.

Это происходит до того, как отработает React-компонент <ErrorBoundary> и до того, как фреймворк смонтирует первый DOM-узел в страницу. В результате происходит неперехваченное исключение (Uncaught Exception), выполнение JavaScript-бандла аварийно прекращается, рендерер не стартует, а пользователь остаётся один на один с белым экраном.

Как починить

Шаг 1. Безопасная обёртка SafeStorage. Никогда не обращайтесь к window.localStorage напрямую. Создайте модуль безопасного хранилища, который перехватывает ошибки песочницы iOS и прозрачно переключается на оперативную память (in-memory Map):

typescript
// safeStorage.ts — безопасная работа с хранилищем в iOS WebKit
class SafeStorage implements Storage {
  private memory = new Map<string, string>();
  private isAvailable: boolean;

  constructor() {
    this.isAvailable = this.checkStorage();
  }

  private checkStorage(): boolean {
    try {
      if (typeof window === 'undefined' || !window.localStorage) return false;
      const testKey = '__tma_storage_test__';
      window.localStorage.setItem(testKey, '1');
      window.localStorage.removeItem(testKey);
      return true;
    } catch {
      // WebKit заблокировал доступ к хранилищу из-за настроек приватности
      return false;
    }
  }

  get length(): number {
    return this.isAvailable ? window.localStorage.length : this.memory.size;
  }

  getItem(key: string): string | null {
    if (this.isAvailable) {
      try {
        return window.localStorage.getItem(key);
      } catch {
        return this.memory.get(key) ?? null;
      }
    }
    return this.memory.get(key) ?? null;
  }

  setItem(key: string, value: string): void {
    if (this.isAvailable) {
      try {
        window.localStorage.setItem(key, value);
        return;
      } catch {
        // Fallback в память, если квота переполнена или права отозваны
      }
    }
    this.memory.set(key, String(value));
  }

  removeItem(key: string): void {
    if (this.isAvailable) {
      try {
        window.localStorage.removeItem(key);
      } catch {}
    }
    this.memory.delete(key);
  }

  clear(): void {
    if (this.isAvailable) {
      try {
        window.localStorage.clear();
      } catch {}
    }
    this.memory.clear();
  }

  key(index: number): string | null {
    if (this.isAvailable) {
      try {
        return window.localStorage.key(index);
      } catch {}
    }
    return Array.from(this.memory.keys())[index] ?? null;
  }
}

export const safeStorage = new SafeStorage();

Шаг 2. Переход на нативный Telegram CloudStorage. Для долговременного сохранения пользовательских настроек и состояния корзины используйте нативный API Telegram: Telegram.WebApp.CloudStorage. Он хранит данные на серверах Telegram, привязан к идентификатору аккаунта, не зависит от настроек кук Safari и синхронизируется между смартфоном и десктопом пользователя.

Ошибка №3. Сбои валидации initData: почему бэкенд возвращает 401 и вешает фронтенд

При запуске Mini App клиент Telegram передаёт приложению строку инициализации: window.Telegram.WebApp.initData.

Она содержит профиль пользователя (id, имя, username, язык интерфейса), время генерации строки auth_date и криптографическую подпись hash. Бэкенд обязан проверить подлинность этих данных, чтобы злоумышленник не мог подделать чужой user_id.

Если валидация завершается неудачей, бэкенд возвращает статус 401 Unauthorized. Если на фронтенде не предусмотрен явный экран ошибки авторизации, пользователь видит вечный скелетон или пустой экран.

4 подводных камня валидации HMAC-SHA256

  1. Неверный секретный ключ. Распространённая ошибка новичков — вычисление HMAC от токена бота напрямую. По спецификации Telegram секретный ключ вычисляется как HMAC-SHA256 от токена бота, где ключом хеширования выступает фиксированная константная строка "WebAppData":
    secret_key = HMAC_SHA256(key="WebAppData", msg=BOT_TOKEN).
  2. Алфавитная сортировка параметров. Перед склеиванием параметров в строку data_check_string через символ переноса строки \n, все пары key=value обязаны быть отсортированы строго лексикографически по имени ключа. Параметр hash при этом обязательно исключается.
  3. Протухание сессии по auth_date. Telegram генерирует initData один раз при первом открытии Mini App. Если пользователь оставил приложение свернутым на 2 часа и вернулся к оформлению заказа, таймштамп auth_date устаревает. Если на бэкенде установлен слишком жесткий таймаут (например, 15 или 30 минут), все последующие API-запросы будут отвергнуты.
  4. Кодирование спецсимволов и кириллицы. Поле user передаётся внутри initData в виде экранированной JSON-строки. Если ваш HTTP-фреймворк производит автоматическое двойное URL-декодирование пробелов, кавычек или эмодзи, исходная строка проверки изменится, и рассчитанный хэш не сойдётся с оригиналом.

Эталонный алгоритм валидации на Python

Ниже приведена надежная реализация проверки подписи для FastAPI или любого другого Python-бэкенда:

python
import hmac
import hashlib
import json
import time
from urllib.parse import parse_qsl

def validate_telegram_init_data(init_data: str, bot_token: str, max_age_seconds: int = 86400) -> dict:
    """
    Проверяет подлинность initData от Telegram Mini App.
    Возвращает словарь с параметрами при успехе.
    Выбрасывает ValueError при фальсификации данных или протухании сессии.
    """
    if not init_data:
        raise ValueError("Пустая строка initData")

    # 1. Разбираем параметры строки запроса
    parsed_data = dict(parse_qsl(init_data, keep_blank_values=True))

    if "hash" not in parsed_data:
        raise ValueError("В параметрах initData отсутствует обязательное поле hash")

    received_hash = parsed_data.pop("hash")

    # 2. Проверяем срок жизни данных
    auth_date = int(parsed_data.get("auth_date", 0))
    if time.time() - auth_date > max_age_seconds:
        raise ValueError("Сессия устарела (auth_date expired). Требуется перезапуск приложения.")

    # 3. Собираем data_check_string с алфавитной сортировкой ключей
    check_items = [f"{k}={v}" for k, v in sorted(parsed_data.items())]
    data_check_string = "\n".join(check_items)

    # 4. Вычисляем секретный ключ: HMAC-SHA256("WebAppData", bot_token)
    secret_key = hmac.new(b"WebAppData", bot_token.encode("utf-8"), hashlib.sha256).digest()

    # 5. Вычисляем контрольный хэш от собранной строки
    calculated_hash = hmac.new(secret_key, data_check_string.encode("utf-8"), hashlib.sha256).hexdigest()

    # 6. Защищенное от атак по времени сравнение хэшей
    if not hmac.compare_digest(calculated_hash, received_hash):
        raise ValueError("Фальсификация подписи: контрольный хэш не совпадает")

    # 7. Преобразуем JSON-поле user в словарь
    if "user" in parsed_data:
        parsed_data["user"] = json.loads(parsed_data["user"])

    return parsed_data

Ошибка №4. Вьюпорт iOS, сдвиг экранной клавиатуры и заблокированный скролл

В веб-разработке традиционно используется правило height: 100vh или 100% для задания высоты экрана. Однако внутри браузерного движка WebKit на iPhone это правило приводит к двум фатальным проблемам интерфейса:

  1. Сдвиг экрана при открытии клавиатуры. Когда пользователь ставит курсор в поле ввода (<input> или <textarea>), операционная система iOS поднимает виртуальную клавиатуру и сдвигает контейнер WebView вверх. Когда клавиатура закрывается (по нажатию «Готово» или тапу вне поля), WebKit не возвращает вьюпорт в исходное положение. В результате нижняя навигационная панель и кнопка «Оплатить заказ» физически улетают за пределы экрана.
  2. Паразитный вертикальный скролл (Rubber Banding). Высота 100vh в Safari включает высоту строки состояния и системной панели Home Bar. Приложение не помещается в экран, и при попытке прокрутки интерфейс дёргается и пружинит вверх-вниз.

Как починить

1. Вызов метода expand(). Сразу после вызова Telegram.WebApp.ready() обязательно вызовите метод expand(). Он разворачивает Mini App на всю доступную высоту экрана смартфона:

javascript
if (window.Telegram?.WebApp) {
  const tg = window.Telegram.WebApp;
  tg.ready();
  tg.expand();
}

2. Использование CSS-переменных Telegram. Telegram WebApp SDK автоматически передаёт в стили документа вычисленные значения высоты: --tg-viewport-height и --tg-viewport-stable-height.

Настройте корневой контейнер вашего приложения в CSS:

css
/* Контейнер приложения Telegram Mini App */
.tma-wrapper {
  min-height: var(--tg-viewport-stable-height, 100vh);
  height: var(--tg-viewport-height, 100vh);
  width: 100%;
  overflow-x: hidden;
  overflow-y: auto;
  /* Блокируем паразитную оттяжку страницы в WebKit */
  overscroll-behavior-y: none;
  /* Учитываем системную плашку iPhone Home Bar */
  padding-bottom: env(safe-area-inset-bottom, 16px);
}

3. Корректный мета-тег Viewport. В файле index.html или в метаданных корневого лейаута задайте жесткий вьюпорт без возможности случайного масштабирования жестами:

html
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">

Ошибка №5. Баги BotFather и почему приложение открывается во внешнем браузере

Распространённая жалоба пользователей: «Я нажимаю кнопку в боте, но вместо аккуратного окна внутри Telegram у меня открывается Safari или Chrome, а там сайт выдаёт ошибку».

Причина банальна: кнопка в Telegram настроена как обычный переход по ссылке (url), а не как вызов Web App.

Когда кнопка передаёт обычный URL:

python
# ОШИБКА: открывает системный браузер вместо Mini App
InlineKeyboardButton(text="Открыть каталог", url="https://app.site.ru")

Telegram открывает внешний браузер операционной системы. В нём полностью отсутствует объект window.Telegram.WebApp (он равен undefined). Если ваш код обращается к Telegram.WebApp.ready() без проверки существования объекта, скрипт падает с ошибкой TypeError: Cannot read properties of undefined (reading 'ready').

Как настроить кнопки правильно

В коде бота (на примере библиотеки aiogram 3.x) кнопка должна быть явно типизирована через объект WebAppInfo:

python
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton, WebAppInfo

# ПРАВИЛЬНО: открывает Mini App внутри Telegram
keyboard = InlineKeyboardMarkup(inline_keyboard=[
    [
        InlineKeyboardButton(
            text="🛍️ Открыть магазин",
            web_app=WebAppInfo(url="https://app.vash-domen.ru")
        )
    ]
])

Для настройки постоянной кнопки меню в левом нижнем углу чата откройте @BotFather:

  1. Отправьте команду /setmenubutton.
  2. Выберите вашего бота.
  3. Отправьте прямую HTTPS-ссылку на веб-приложение.

Как подключить отладчик прямо на смартфоне

Если на iPhone приложение всё равно не открывается, а подключать телефон к компьютеру через кабель и настраивать Safari Web Inspector нет времени, используйте мобильную консоль Eruda.

Добавьте в точку входа вашего приложения скрипт условного подключения отладчика:

javascript
// Подключение мобильного отладчика по секретному параметру ?debug=1
if (typeof window !== 'undefined' && new URLSearchParams(window.location.search).has('debug')) {
  const script = document.createElement('script');
  script.src = 'https://cdn.jsdelivr.net/npm/eruda';
  script.onload = () => {
    if (window.eruda) window.eruda.init();
  };
  document.head.appendChild(script);
}

Теперь, если в ссылке на Web App передать параметр https://app.vash-domen.ru/?debug=1, прямо поверх интерфейса на экране смартфона появится плавающая кнопка консоли разработчика, в которой видны все сетевые запросы, ошибки в консоли и дерево DOM.

Ошибка №6. Неполная цепочка SSL (Intermediate CA) и строгий WebKit

Симптомы этой проблемы сбивают с толку даже опытных системных администраторов:

  • На ноутбуке в Chrome сайт открывается с зелёным замочком.
  • На Android-смартфоне всё работает быстро и без ошибок.
  • На iPhone внутри Telegram Mini App — вечный белый экран, а в логах веб-сервера access.log нет вообще ни одного запроса.

Почему это происходит

Десктопные браузеры умеют самостоятельно восстанавливать неполную цепочку доверия сертификатов с помощью механизма AIA (Authority Information Access): если веб-сервер не отдал промежуточный сертификат (Intermediate CA), браузер докачивает его самостоятельно.

Движок Apple WebKit на iOS не делает фоновую дозагрузку сертификатов ради экономии трафика и безопасности. Если в конфигурации веб-сервера Nginx указан только листовой сертификат домена (cert.pem), а не полная цепочка (fullchain.pem), iOS разрывает TLS-рукопожатие ещё до отправки HTTP-запроса.

Как проверить и исправить

Проверьте ваш домен через терминал с помощью утилиты openssl:

bash
openssl s_client -connect app.vash-domen.ru:443 -servername app.vash-domen.ru

Если в выводе присутствует строка Verify return code: 21 (unable to verify the first certificate) — цепочка сертификата разорвана.

В конфигурационном файле Nginx обязательно укажите файл с полной цепочкой сертификатов:

nginx
# ОШИБКА: листовой сертификат вызовет белый экран на iOS
# ssl_certificate /etc/letsencrypt/live/app.vash-domen.ru/cert.pem;

# ПРАВИЛЬНО: полная цепочка доверия для всех устройств
ssl_certificate /etc/letsencrypt/live/app.vash-domen.ru/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/app.vash-domen.ru/privkey.pem;

Чек-лист проверки Telegram Mini App перед запуском трафика

Используйте этот контрольный чеклист перед релизом или запуском рекламной кампании:

  • Тестирование на 4 средах исполнения: проверено открытие в iOS (WebKit), Android (Chromium), Telegram Desktop и Telegram Web.
  • Корректные заголовки фрейма: в Content-Security-Policy прописана директива frame-ancestors https://*.telegram.org, заголовок X-Frame-Options отключен.
  • Безопасная работа с хранилищем: вызовы localStorage обёрнуты в безопасный полифилл try/catch, долговременные данные переведены на CloudStorage.
  • Валидация initData на сервере: ключ считается через HMAC-SHA256("WebAppData", bot_token), параметры отсортированы по алфавиту, таймаут сессии адекватен поведению пользователей.
  • Адаптация вьюпорта: вызван Telegram.WebApp.expand(), для корневого контейнера задана высота var(--tg-viewport-height, 100vh), сброшен скролл overscroll-behavior-y: none.
  • Проверка SSL-цепочки: на веб-сервере установлен бандл fullchain.pem, тест SSL Labs показывает рейтинг A/A+.
  • Мобильный отладчик: внедрена возможность запуска консоли Eruda по секретному параметру для оперативной диагностики на реальных устройствах.

Когда разработку и аудит стоит доверить инженерам «2САЙТ»

Telegram Mini App сегодня — это мощнейший инструмент продаж и обслуживания клиентов, позволяющий бизнесу отказаться от дорогой разработки отдельных мобильных приложений под iOS и Android. Однако за кажущейся простотой веб-страницы скрываются жесткие технические ограничения гибридных систем, специфики Apple WebKit и строгой политики безопасности мессенджера.

Если вы планируете запуск или ваш текущий Mini App работает нестабильно, теряет пользователей на этапе входа и сливает рекламный бюджет:

Для консультации и проведения технического аудита вашего Telegram Mini App свяжитесь с нашими инженерами через форму на сайте или Telegram-бота студии. Мы выявим скрытые уязвимости архитектуры до того, как они повлияют на ваших клиентов.