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 могут обозначать три разных способа доставки. Выбор определяет, кто получает байты файла:
| Значение | Что происходит | Типичная ошибка |
|---|---|---|
| HTTP-ссылка | Telegram сам скачивает файл с указанного сервера. | Ссылка ведёт на страницу просмотра или требует авторизации. |
| file_id | Telegram повторно использует уже известное этому боту вложение. | 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, ничего не отправляет боту и не сохраняет скачанное содержимое:
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-пример ниже — каждый запуск создаёт новое сообщение.
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 и не повторяет отправку при сбое:
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 и ограничения отправки.
Как проверить исправление
- Отправьте небольшой локальный документ без подписи в тестовый чат и проверьте содержимое.
- Сохраните file_id из успешного ответа и отдельно проверьте повторное использование тем же ботом.
- Для URL проверьте подходящий тип файла и прямой доступ без авторизации. Если загрузка байтов проходит, а URL — нет, ищите причину в доступе к источнику и требованиях URL-отправки.
- Верните рабочий файл и сравните его с тестовым: размер, формат, имя и время передачи.
- Добавляйте подпись и остальные параметры по одному, чтобы увидеть, что меняет результат.
- Убедитесь, что ошибка не запускает автоматическую повторную отправку без разбора результата.
Если маленький файл работает только из консоли, сравните окружение 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.
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.
Источники
- Telegram: отправка файлов — способы передачи и ограничения.
- sendPhoto и sendDocument — параметры конкретных методов.
- PHP: CURLFile — загрузка файла через расширение curl.
- BotGate PHP SDK — multipart и обработка ошибок.
Примеры рассчитаны на один небольшой тестовый файл и одну попытку отправки. Потоковая обработка пользовательских загрузок, проверка их содержимого и очередь больших файлов требуют отдельной реализации в приложении.