Перейти к содержанию
12 минут чтения
Тема: Диагностика Telegram Bot API

Telegram-бот не отправляет фото или документ: проверяем URL, file_id и загрузку файла

Бот отправляет текст, но фотография или PDF не доходят: Telegram отклоняет ссылку, не узнаёт file_id или сообщает об ошибке загрузки. Разберём, чем отличаются эти способы отправки, как найти причину по ответу API и как загрузить небольшой документ через curl и обычный PHP.

1. Начните с метода и ответа API

Зафиксируйте метод, способ передачи файла, HTTP-статус и поля error_code, description из JSON-ответа Telegram. Не записывайте токен из URL запроса, содержимое документа и закрытые ссылки в общий журнал. Слова «не отправляется файл» могут описывать разные сбои:

Как различить причины ошибки отправки файла
НаблюдениеЧто проверить
Нет ответа Telegram, соединение оборвалосьДоступ к API, таймаут, длительность и размер загрузки. Результат отправки может быть неизвестен.
Описание говорит о URL или скачиванииДоступ к файлу без cookies, ответ сервера, перенаправления и фактический тип содержимого.
Описание говорит о file identifierКак получен file_id, какому боту и типу вложения он принадлежит.
Описание говорит о размере или изображенииОграничения метода, размер файла, формат и параметры изображения.
chat not found, запрет отправки, 429 или ошибка entitiesЧат и права, лимит запросов или разметку подписи. Причина может быть не в самом файле.

Для первого теста уберите caption, parse_mode, caption_entities и дополнительные параметры. Возьмите небольшой файл без персональных данных и собственный тестовый чат. Если без подписи отправка проходит, проверьте разметку текста. Если не проходит даже обычное сообщение, начните с chat_id и прав бота.

2. Уточните, что именно вы передаёте

Параметр photo у sendPhoto и параметр document у sendDocument могут обозначать три разных способа доставки. Выбор определяет, кто получает байты файла:

Три способа отправки файла через Telegram Bot API
ЗначениеЧто происходитТипичная ошибка
HTTP-ссылкаTelegram сам скачивает файл с указанного сервера.Ссылка ведёт на страницу просмотра или требует авторизации.
file_idTelegram повторно использует уже известное этому боту вложение.ID взят у другого бота или перепутан с file_unique_id.
multipart/form-dataВаше приложение загружает содержимое файла в запросе.Вместо содержимого отправлена строка с путём.

Значение /var/app/report.pdf в JSON не даёт обычному облачному Bot API доступ к диску приложения. Для локального файла нужна multipart-загрузка. Отдельный локальный сервер Telegram Bot API имеет свои возможности; примеры ниже рассчитаны на api.telegram.org.

Сопоставляйте ограничения именно с методом и способом передачи по документации отправки файлов. Лимит загрузки через multipart нельзя автоматически применять к скачиванию по URL. Для sendPhoto важны также размеры и пропорции изображения; смена расширения файла не меняет его формат.

3. Проверьте ссылку без браузерной сессии

Успешное открытие в браузере может зависеть от вашей авторизации. Telegram не получает cookies браузера, заголовок Authorization или доступ к localhost и внутренней сети приложения. Ссылка должна возвращать сам файл, а не HTML-страницу с кнопкой «Скачать».

Для диагностики выполните GET своего небольшого публичного тестового файла. HEAD бывает недостаточно: сервер может иначе обрабатывать запрос без тела ответа. Команда ниже предназначена для Bash и curl, ничего не отправляет боту и не сохраняет скачанное содержимое:

Bash · проверка публичной HTTPS-ссылки
read -rp 'URL небольшого публичного тестового файла: ' FILE_URL
curl -q --silent --show-error \
  --location --max-redirs 3 \
  --proto '=https' --proto-redir '=https' \
  --connect-timeout 5 --max-time 20 \
  --dump-header - --output /dev/null \
  --write-out '\nHTTP %{http_code}; скачано %{size_download} байт\n' \
  --url "$FILE_URL"
unset FILE_URL

Проверьте конечный HTTP 200 и Content-Type. Ответ 200 с text/html часто означает страницу входа или ошибки. Просмотрите цепочку перенаправлений: она не должна заканчиваться авторизацией, защитной страницей CDN или истёкшей подписанной ссылкой. Не публикуйте диагностические заголовки, если в них есть закрытые адреса или cookies.

Успех curl на вашем сервере не доказывает доступность из сети Telegram. Сравните журнал запросов на сервере файла: дошёл ли запрос при попытке отправки, какой статус вернули приложение, CDN или защита от ботов. При TLS-ошибке исправьте сертификат и цепочку; не обходите проверку через отключение TLS-валидации.

По описанию sendDocument, отправка документа по URL поддерживается для PDF и ZIP. Для других типов начните с multipart. Для фотографии сервер должен отдавать изображение с подходящим MIME-типом, а не только URL с окончанием .jpg.

Если источник требует авторизации, приложение может получить файл своим разрешённым способом и загрузить его через multipart. Не превращайте такой обработчик в загрузчик произвольных пользовательских URL: используйте разрешённые источники и ограничения размера и времени.

4. Не путайте file_id и file_unique_id

После успешной отправки документа возьмите result.document.file_id из ответа. Для фотографии Telegram возвращает массив result.photo с вариантами размера и их file_id. Сохранённый file_id можно передать тому же боту в соответствующем методе без повторной загрузки байтов.

  • Храните ID вместе с идентификатором бота и типом вложения. Кеш «один file_id на файл для всех ботов» не подходит.
  • Не используйте file_unique_id для отправки или скачивания: он служит для сопоставления файлов.
  • Не подставляйте вместо file_id путь из getFile, имя файла или URL.
  • Не пытайтесь превратить документ в фотографию простой передачей его file_id в sendPhoto. Для смены способа представления загрузите исходный файл нужным методом.

Если ID перестал подходить после смены бота, загрузите тестовый исходник от имени текущего бота и сравните результат. Обновляйте кеш после подтверждённого успеха. Не запускайте бесконечную цепочку «ошибка ID → загрузка → повтор»: сначала установите причину и ограничьте число попыток.

5. Проверьте multipart на небольшом документе

Создайте в текущем каталоге файл example.txt с любым коротким тестовым текстом. У бота должен быть доступ к выбранному чату. Этот пример отправляет один настоящий документ; используйте собственный тестовый чат. Можно выбрать его или PHP-пример ниже — каждый запуск создаёт новое сообщение.

Bash · одна отправка example.txt
set +x
read -rsp 'Telegram Bot Token: ' TG_TOKEN
printf '\n'
read -rp 'chat_id тестового чата: ' CHAT_ID

printf 'url = "https://api.telegram.org/bot%s/sendDocument"\n' "$TG_TOKEN" |
  curl -q --config - --silent --show-error \
    --connect-timeout 5 --max-time 60 \
    --form-string "chat_id=$CHAT_ID" \
    --form "document=@./example.txt;type=text/plain" \
    --write-out '\nHTTP %{http_code}\n'

unset TG_TOKEN CHAT_ID

Токен вводится скрыто и передаётся curl через стандартный ввод. Не включайте трассировку команд или verbose-логирование с секретным URL. Успех проверяется по ok: true в JSON, а не только по завершению процесса curl.

В этом синтаксисе curl символ @ означает чтение локального файла. В PHP аналогичную задачу решает CURLFile. Не задавайте заголовок Content-Type: application/json для такого запроса: границу частей multipart должен сформировать HTTP-клиент.

Для проверки фотографии используйте настоящий небольшой JPEG, метод sendPhoto, поле photo и MIME image/jpeg. Текстовый файл с расширением .jpg фотографией не становится. Если маленький файл проходит, а рабочий — нет, сравните размер, содержимое, ограничения метода и длительность передачи.

Пример загрузки документа на PHP без SDK

Нужны PHP 8.2+ с расширениями curl и fileinfo. Положите example.txt рядом со скриптом, а процессу PHP передайте TELEGRAM_BOT_TOKEN и TELEGRAM_CHAT_ID через настройки окружения. Сам PHP не читает .env автоматически.

Сохраните код в send-document.php, добавив в начале <?php. Пример делает одну попытку, проверяет ответ API и не повторяет отправку при сбое:

PHP · send-document.php
declare(strict_types=1);

$requiredEnv = static function (string $name): string {
    $value = getenv($name);
    if ($value === false || trim($value) === '') {
        throw new RuntimeException('Не задана переменная '.$name);
    }
    return $value;
};

$token = trim($requiredEnv('TELEGRAM_BOT_TOKEN'));
$chatId = trim($requiredEnv('TELEGRAM_CHAT_ID'));
$path = realpath(__DIR__.'/example.txt');
if ($path === false || !is_file($path) || !is_readable($path)) {
    throw new RuntimeException('Не найден доступный для чтения example.txt.');
}
$mime = (new finfo(FILEINFO_MIME_TYPE))->file($path);
if ($mime === false) {
    throw new RuntimeException('Не удалось определить MIME-тип.');
}

$curl = curl_init('https://api.telegram.org/bot'.$token.'/sendDocument');
if ($curl === false) {
    throw new RuntimeException('Не удалось создать HTTP-клиент.');
}
curl_setopt_array($curl, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CONNECTTIMEOUT => 5,
    CURLOPT_TIMEOUT => 60,
    CURLOPT_POSTFIELDS => [
        'chat_id' => $chatId,
        'document' => new CURLFile($path, $mime, basename($path)),
    ],
]);

$body = curl_exec($curl);
$status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
if (!is_string($body)) {
    fwrite(STDERR, "Ответ не получен. Уточните результат перед повторной отправкой.\n");
    exit(1);
}

try {
    $response = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
} catch (JsonException) {
    fwrite(STDERR, "Получен ответ не в формате JSON, HTTP $status. Отправка не подтверждена.\n");
    exit(1);
}

if (!is_array($response) || $status < 200 || $status >= 300 || ($response['ok'] ?? false) !== true) {
    $description = is_array($response) ? ($response['description'] ?? null) : null;
    fwrite(STDERR, 'HTTP '.$status.': '.(is_string($description) ? $description : 'Отправка не подтверждена.')."\n");
    exit(1);
}

$result = $response['result'] ?? null;
$document = is_array($result) ? ($result['document'] ?? null) : null;
$fileId = is_array($document) ? ($document['file_id'] ?? null) : null;
echo "Telegram подтвердил отправку документа.\n";
if (is_string($fileId)) {
    echo 'file_id для этого бота: '.$fileId."\n";
}

Запуск: php send-document.php. Проверьте документ в чате: имя, содержимое и размер. CURLFile открывает именно указанный локальный файл; передача обычной строки пути в POSTFIELDS не заменяет этот объект. Не сериализуйте массив POSTFIELDS в JSON.

После таймаута не считайте файл гарантированно неотправленным. Telegram мог принять документ, пока ответ потерялся. Политику повторов разбирает статья о дублирующихся сообщениях. При явном 429 учитывайте retry_after и ограничения отправки.

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

  1. Отправьте небольшой локальный документ без подписи в тестовый чат и проверьте содержимое.
  2. Сохраните file_id из успешного ответа и отдельно проверьте повторное использование тем же ботом.
  3. Для URL проверьте подходящий тип файла и прямой доступ без авторизации. Если загрузка байтов проходит, а URL — нет, ищите причину в доступе к источнику и требованиях URL-отправки.
  4. Верните рабочий файл и сравните его с тестовым: размер, формат, имя и время передачи.
  5. Добавляйте подпись и остальные параметры по одному, чтобы увидеть, что меняет результат.
  6. Убедитесь, что ошибка не запускает автоматическую повторную отправку без разбора результата.

Если маленький файл работает только из консоли, сравните окружение worker-а или PHP-FPM: путь, права чтения, доступные расширения, сетевые ограничения и конфигурацию таймаутов. Относительный путь может указывать в другой каталог; __DIR__ в примере устраняет эту неоднозначность.

Если вы используете BotGate

Правила выбора URL, file_id и типа вложения остаются теми же. Для URL или file_id передавайте соответствующую строку в photo или document через обычный вызов метода. При URL-отправке Telegram по-прежнему должен получить файл с указанного адреса: шлюз не делает закрытую ссылку доступной.

Для локального файла PHP SDK использует MultipartField. Его метод file принимает содержимое или поток, а не путь. Подготовьте bootstrap.php из руководства PHP: он создаёт $bot и $requiredEnv с отключёнными автоматическими повторами. Рядом положите example.txt.

PHP · send-document-botgate.php
declare(strict_types=1);

use BotGate\Exception\BotGateException;
use BotGate\Exception\TelegramException;
use BotGate\Http\MultipartField;

require __DIR__.'/bootstrap.php';

$stream = fopen(__DIR__.'/example.txt', 'rb');
if ($stream === false) {
    throw new RuntimeException('Не удалось открыть тестовый файл.');
}

$exitCode = 0;
try {
    $bot->callMultipart('sendDocument', [
        MultipartField::field('chat_id', $requiredEnv('TELEGRAM_CHAT_ID')),
        MultipartField::file('document', $stream, 'example.txt', 'text/plain'),
    ]);
    echo "Telegram подтвердил отправку документа.\n";
} catch (TelegramException $error) {
    fwrite(STDERR, 'Telegram '.($error->telegramErrorCode() ?? 'без кода').': '.$error->getMessage()."\n");
    $exitCode = 1;
} catch (BotGateException $error) {
    fwrite(STDERR, "Отправка не подтверждена. Проверьте ответ API перед повтором.\n");
    $exitCode = 1;
} finally {
    fclose($stream);
}
exit($exitCode);

Файл должен начинаться с <?php. Команда php send-document-botgate.php отправляет один документ. Строка пути вместо $stream загрузила бы текст пути, поэтому передавайте открытый ресурс. Пример multipart-запроса и формат ошибок есть в документации API; ошибки Telegram SDK предоставляет через TelegramException.

Источники

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