Когда мы работаем с внешними веб-ресурсами из 1С, часто возникает необходимость получения данных по протоколу HTTP или HTTPS. Объекты HTTPСоединение и HTTPЗапрос предоставляют нам мощный инструментарий для этого. Однако, не всегда все проходит гладко. Одной из распространенных проблем, с которой сталкиваются разработчики, является обработка HTTP-редиректов, особенно кода состояния 301 "Moved Permanently". На форуме 1С была поднята именно такая проблема, когда попытка получить файл по HTTP-запросу постоянно возвращала код 301, несмотря на попытки обработать редирект. Давайте подробно разберем, почему это происходит и как можно эффективно бороться с такими перенаправлениями.
Наш пользователь столкнулся с ситуацией, когда его код для загрузки файла из интернета, который ранее работал без проблем, внезапно начал возвращать код состояния 301. Исходный код выглядел примерно следующим образом:
HTTP = Новый HTTPСоединение(НастройкаОбмена.СерверИсточник, , , , , , Новый ЗащищенноеСоединениеOpenSSL(Неопределено, Неопределено));
Попытка
Результат = HTTP.Получить(СтрокаПараметраПолучения, ИмяВходящегоФайла);
Если Результат.КодСостояния = 200 Тогда
Возврат ИмяВходящегоФайла;
ИначеЕсли Результат.КодСостояния >= 301 Тогда
НомерНачалаАдреса = СтрНайти(Результат.Заголовки["Location"], "/", , , 3);
СерверИсточник = Сред(Результат.Заголовки["Location"], 9, НомерНачалаАдреса - 9);
СтрокаПараметраПолучения = СтрЗаменить(Результат.Заголовки["Location"], "https://" + СерверИсточник, "");
HTTPЗагрузки = ПолучитьHTTPСоединение(СерверИсточник);
Результат = HTTPЗагрузки.Получить(СтрокаПараметраПолучения, ИмяВходящегоФайла);
Если Результат.КодСостояния = 200 Тогда
Возврат ИмяВходящегоФайла;
КонецЕсли;
КонецЕсли;
Исключение
Сообщение.Текст = ОписаниеОшибки();
Сообщение.Сообщить();
Возврат "";
КонецПопытки;
В этом коде мы видим попытку вручную обработать редирект: если код состояния Результат.КодСостояния больше или равен 301, то предполагается, что это редирект, и новый адрес извлекается из заголовка Location. Затем формируется новое HTTP-соединение и запрос. Однако, пользователь сообщает, что даже после этой обработки он продолжает получать 301 код состояния, что указывает на некорректность или неполноту текущего механизма.
Мы видим, что в коде используется Новый ЗащищенноеСоединениеOpenSSL(Неопределено, Неопределено), что подразумевает работу по протоколу HTTPS. Это важный нюанс, поскольку обработка защищенных соединений требует правильной настройки, хотя в данном случае проблема оказалась не в этом.
Прежде чем углубляться в решение, давайте вспомним, что означают коды состояния HTTP в диапазоне 3xx. Эти коды не являются ошибками (как, например, 4xx или 5xx), а указывают на необходимость выполнения клиентом дальнейших действий для завершения запроса. Они используются сервером для перенаправления запроса на другой адрес.
В исходном коде используется условие Результат.КодСостояния >= 301. Это условие является слишком общим и потенциально опасным. Оно будет захватывать не только редиректы (301-308), но и все клиентские (4xx) и серверные (5xx) ошибки. Например, 404 Not Found или 500 Internal Server Error будут трактоваться как редиректы, что приведет к некорректной логике. Правильнее было бы проверять диапазон кодов редиректов, например: (Результат.КодСостояния >= 300) И (Результат.КодСостояния < 400).
В процессе обсуждения выяснилось, что адрес, на который происходит редирект, выглядел так:
"Location": "https://funsamokat.ru/catalog/pricelist"
При этом исходный запрос мог быть сделан на адрес, например, https://funsamokat.ru/catalog/pricelist/. Обратите внимание на отсутствие завершающего слеша в заголовке Location. Многие веб-серверы, такие как Nginx (который, судя по заголовкам, использовался на сервере funsamokat.ru), по умолчанию настроены на то, чтобы при запросе каталога без завершающего слеша (например, /catalog/pricelist) выдавать 301-й редирект на тот же самый адрес, но уже с завершающим слешем (/catalog/pricelist/). Это делается для поддержания консистентности URL, предотвращения проблем с SEO и корректной обработки относительных путей.
Таким образом, когда ваш код извлекал адрес из заголовка Location, он получал https://funsamokat.ru/catalog/pricelist. Затем он формировал новое HTTP-соединение с этим же адресом и отправлял запрос. Но так как в этом адресе по-прежнему отсутствовал завершающий слеш, сервер снова выдавал 301-й редирект на https://funsamokat.ru/catalog/pricelist/. Это создавало бесконечный или, по крайней мере, повторяющийся цикл редиректов на один и тот же "некорректный" (без слеша) адрес, не давая запросу завершиться с кодом 200.
Как это часто бывает, решение оказалось довольно простым. Пользователь форума подсказал добавить завершающий слеш к адресу в запросе. Это мгновенно решило проблему.
Когда мы явно указываем завершающий слеш в СтрокаПараметраПолучения, например, /catalog/pricelist/, мы сразу обращаемся к корректному адресу каталога, и сервер не выдает редирект. Запрос успешно завершается с кодом 200.
Модифицированный фрагмент кода может выглядеть следующим образом:
// В момент первого вызова или при формировании СтрокаПараметраПолучения
// Убедитесь, что СтрокаПараметраПолучения заканчивается на "/"
// Например:
// Если НЕ Прав(СтрокаПараметраПолучения, 1) = "/" Тогда
// СтрокаПараметраПолучения = СтрокаПараметраПолучения + "/";
// КонецЕсли;
HTTP = Новый HTTPСоединение(НастройкаОбмена.СерверИсточник, , , , , , Новый ЗащищенноеСоединениеOpenSSL(Неопределено, Неопределено));
Попытка
Результат = HTTP.Получить(СтрокаПараметраПолучения, ИмяВходящегоФайла);
Если Результат.КодСостояния = 200 Тогда
Возврат ИмяВходящегоФайла;
ИначеЕсли (Результат.КодСостояния >= 300) И (Результат.КодСостояния < 400) Тогда // Более корректная проверка на редирект
// Далее идет логика обработки редиректа, если он все-таки произошел
// (например, на совершенно другой адрес)
// Для рассмотренной проблемы с funsamokat.ru, этот блок уже не понадобится
// при правильном формировании СтрокаПараметраПолучения изначально.
// Тем не менее, для общей универсальности, разберем эту часть подробнее.
// Извлекаем новый сервер и путь из заголовка Location
НовыйАдрес = Результат.Заголовки["Location"];
// Важно: Location может содержать как полный URL, так и относительный путь.
// Мы должны корректно его разобрать.
Если Найти(НовыйАдрес, "://") = 0 Тогда // Относительный путь
// Допустим, мы работаем с тем же сервером, только меняем путь
// Это упрощенная логика, реальный парсинг может быть сложнее
СтрокаПараметраПолучения = НовыйАдрес;
Иначе // Полный URL
// Пример упрощенного парсинга для HTTPS
НачалоСервера = Найти(НовыйАдрес, "://") + 3; // Игнорируем "https://" или "http://"
НомерНачалаПути = СтрНайти(НовыйАдрес, "/", НачалоСервера);
СерверИсточник = "";
СтрокаПараметраПолучения = "/";
Если НомерНачалаПути > НачалоСервера Тогда // Путь присутствует
СерверИсточник = Сред(НовыйАдрес, НачалоСервера, НомерНачалаПути - НачалоСервера);
СтрокаПараметраПолучения = Сред(НовыйАдрес, НомерНачалаПути);
Иначе // Пути нет, только сервер
СерверИсточник = Сред(НовыйАдрес, НачалоСервера);
СтрокаПараметраПолучения = "/"; // Корневой путь
КонецЕсли;
КонецЕсли;
// Создаем новое HTTPСоединение для нового сервера, если он изменился
// или для текущего, если изменился только путь.
// При этом учитываем, что новое соединение может быть по HTTP, если Location указал HTTP
// В нашем случае, поскольку мы изначально на HTTPS, и редирект на HTTPS,
// будем считать, что протокол сохраняется.
// Функция ПолучитьHTTPСоединение должна уметь создавать соединение
// с новым сервером и портом, а также с ЗащищенноеСоединениеOpenSSL, если это HTTPS.
// Для примера форума, мы используем НастройкаОбмена.СерверИсточник,
// предполагая, что ПолучитьHTTPСоединение() создает объект аналогично
// исходному HTTP = Новый HTTPСоединение(... ЗащищенноеСоединениеOpenSSL...)
// HTTPЗагрузки = ПолучитьHTTPСоединение(СерверИсточник); // Исходная версия
// Если СерверИсточник изменился, нужно создать новое соединение.
// Иначе можно переиспользовать текущее или создать новое, если нужны другие параметры.
// Для более надежной обработки, лучше всегда создавать новое HTTPСоединение
// при изменении СерверИсточник или порта.
// Если СерверИсточник остался тем же (например, только путь изменился),
// можно переиспользовать HTTP, но изменить СтрокаПараметраПолучения.
// В рамках нашей задачи, СерверИсточник не меняется, менялся только путь (наличие слеша).
// Поэтому для решения проблемы, достаточно было убедиться, что СтрокаПараметраПолучения
// всегда содержит завершающий слеш.
// Если же редирект был на совершенно другой сервер, то код должен быть примерно таким:
Если Найти(НовыйАдрес, "https://") = 1 ИЛИ Найти(НовыйАдрес, "http://") = 1 Тогда
// Здесь нужен более сложный парсинг для извлечения Протокола, Хоста, Порта, Пути
// и затем создание нового HTTPСоединение.
// Для краткости, мы полагаемся на то, что СерверИсточник корректно обновляется
// и ПолучитьHTTPСоединение() может создать новое соединение.
// ... код парсинга НовогоАдреса для извлечения Протокола, Хоста, Порта, Пути ...
// HTTPЗагрузки = Новый HTTPСоединение(Хост, Порт, Прокси, Таймаут, ИмяПользователя, Пароль, ЗащищенноеСоединение);
// Далее, после создания HTTPЗагрузки:
Результат = HTTPЗагрузки.Получить(СтрокаПараметраПолучения, ИмяВходящегоФайла);
Если Результат.КодСостояния = 200 Тогда
Возврат ИмяВходящегоФайла;
КонецЕсли;
КонецЕсли;
КонецЕсли;
Исключение
Сообщение.Текст = ОписаниеОшибки();
Сообщение.Сообщить();
Возврат "";
КонецПопытки;
Самое главное, что стоит вынести из этого решения: всегда проверяйте формат URL, особенно наличие завершающего слеша для директорий. Это простая, но часто упускаемая деталь, которая может привести к цикличным редиректам.
Хотя добавление слеша решило конкретную проблему, в более общих случаях вам может потребоваться более универсальный механизм для следования по редиректам. Объекты HTTPСоединение и HTTPЗапрос в 1С не выполняют автоматическое следование по редиректам, поэтому разработчику необходимо реализовать эту логику самостоятельно. Давайте рассмотрим шаги.
Ограничение количества редиректов: Чтобы избежать бесконечного цикла, всегда устанавливайте ограничение на максимальное количество перенаправлений. Например, 5 или 10 попыток.
МаксРедиректов = 5;
ТекущийРедирект = 0;
// ...
Пока (Результат.КодСостояния >= 300 И Результат.КодСостояния < 400) И (ТекущийРедирект < МаксРедиректов) Цикл
ТекущийРедирект = ТекущийРедирект + 1;
// ... логика обработки редиректа ...
КонецЦикла;
Корректный парсинг заголовка Location: Заголовок Location может содержать как полный URL (абсолютный), так и относительный путь. Нам нужно уметь их различать и правильно формировать новый запрос.
Пример парсинга:
Функция РазложитьURL(ПолныйURL)
Результат = Новый Структура("Протокол, Хост, Порт, Путь");
Если Найти(ПолныйURL, "://") > 0 Тогда
ПротоколИОстальное = СтрРазделить(ПолныйURL, "://");
Результат.Протокол = НижРег(ПротоколИОстальное[0]);
Остаток = ПротоколИОстальное[1];
Иначе
Результат.Протокол = "https"; // Или "http" по умолчанию
Остаток = ПолныйURL;
КонецЕсли;
// Находим порт, если есть
ТочкаСЗапятой = Найти(Остаток, ":");
Слеш = Найти(Остаток, "/");
Если ТочкаСЗапятой > 0 И (Слеш = 0 ИЛИ ТочкаСЗапятой < Слеш) Тогда // Порт указан
Результат.Хост = Лев(Остаток, ТочкаСЗапятой - 1);
ПортИПуть = Сред(Остаток, ТочкаСЗапятой + 1);
СлешПослеПорта = Найти(ПортИПуть, "/");
Если СлешПослеПорта > 0 Тогда
Результат.Порт = Число(Лев(ПортИПуть, СлешПослеПорта - 1));
Результат.Путь = Сред(ПортИПуть, СлешПослеПорта);
Иначе
Результат.Порт = Число(ПортИПуть);
Результат.Путь = "/";
КонецЕсли;
Иначе // Порт не указан, используем стандартный
Если Результат.Протокол = "https" Тогда
Результат.Порт = 443;
Иначе
Результат.Порт = 80;
КонецЕсли;
Если Слеш > 0 Тогда
Результат.Хост = Лев(Остаток, Слеш - 1);
Результат.Путь = Сред(Остаток, Слеш);
Иначе
Результат.Хост = Остаток;
Результат.Путь = "/";
КонецЕсли;
КонецЕсли;
Возврат Результат;
КонецФункции
Использование этой функции поможет нам корректно определить все части URL для нового запроса.
Учет изменения метода запроса: Для некоторых кодов редиректа, таких как 303 See Other, метод запроса должен измениться на GET, даже если исходный запрос был POST. Для 307 Temporary Redirect и 308 Permanent Redirect метод запроса должен оставаться прежним.
МетодЗапроса = "GET"; // Исходный метод
Если Результат.КодСостояния = 303 Тогда
МетодЗапроса = "GET";
ИначеЕсли Результат.КодСостояния = 307 ИЛИ Результат.КодСостояния = 308 Тогда
// МетодЗапроса остается прежним
КонецЕсли;
// Далее ВызватьHTTPМетод(МетодЗапроса, ...)
Это требует использования метода ВызватьHTTPМетод вместо Получить или ОтправитьДляОбработки для большей гибкости.
Передача куки (Cookies): Если первый запрос получил куки (через заголовок Set-Cookie), и эти куки необходимы для последующего перенаправленного запроса (например, для поддержания сессии), их необходимо извлечь и передать в заголовке Cookie нового запроса.
// Получение куки из первого ответа
КукиСервера = Результат.Заголовки["Set-Cookie"]; // Если есть
// ... обработка и формирование заголовка "Cookie" для следующего запроса
ЗаголовкиДляНовогоЗапроса.Вставить("Cookie", "имя_куки=значение_куки; ...");
Настройка защищенного соединения: Если редирект происходит на HTTPS, необходимо корректно инициализировать ЗащищенноеСоединениеOpenSSL. Если вы переходите с HTTPS на HTTP, то защищенное соединение не требуется.
Если НовыйПротокол = "https" Тогда
ЗащищенноеСоединение = Новый ЗащищенноеСоединениеOpenSSL(Неопределено, Неопределено);
Иначе
ЗащищенноеСоединение = Неопределено;
КонецЕсли;
НовоеСоединение = Новый HTTPСоединение(НовыйХост, НовыйПорт, , , , , ЗащищенноеСоединение);
Для большинства задач, особенно если вы не хотите погружаться в тонкости ручной обработки редиректов и сопутствующих деталей (куки, изменение метода, парсинг URL), рекомендуется использовать готовые решения из "Библиотеки стандартных подсистем" (БСП) 1С. В частности, функция ИнтернетПоддержкаПользователей.ЗагрузитьСодержимоеИзИнтернет предоставляет универсальный и надежный механизм для работы с HTTP(S) запросами, который автоматически обрабатывает редиректы.
Давайте рассмотрим, как ее использовать:
ДопПараметры = Новый Структура;
ДопПараметры.Вставить("ФорматОтвета", 1); // 1 - текст, 0 - бинарные данные, 2 - файл
ДопПараметры.Вставить("Метод", "GET"); // Можно указать "POST", "PUT" и т.д.
ДопПараметры.Вставить("ФорматДанныхДляОбработки", 1); // 1 - строка, 0 - бинарные данные
ДопПараметры.Вставить("ДанныеДляОбработки", ""); // Тело запроса для POST/PUT
ДопПараметры.Вставить("Заголовки", Новый Соответствие()); // Дополнительные заголовки, если нужны
ДопПараметры.Вставить("ИмяВходящегоФайла", ИмяВходящегоФайла); // Куда сохранять файл
ДопПараметры.Вставить("ВремяОжидания", 60); // Таймаут в секундах
ДопПараметры.Вставить("ИгнорироватьОшибкиSSL", Ложь); // Для тестовых самоподписанных сертификатов
// Если нужно передать логин и пароль для базовой аутентификации
// ДопПараметры.Вставить("Логин", "ВашЛогин");
// ДопПараметры.Вставить("Пароль", "ВашПароль");
// URL, который мы хотим получить
УРЛ = "https://funsamokat.ru/catalog/pricelist/"; // С завершающим слешем!
// Вызов функции
РезультатЗапроса = ИнтернетПоддержкаПользователей.ЗагрузитьСодержимоеИзИнтернет(УРЛ, , , ДопПараметры);
Если РезультатЗапроса.Успех Тогда
// Данные успешно загружены, или файл сохранен в ИмяВходящегоФайла
Возврат ИмяВходящегоФайла; // Если формат ответа был 2 (файл)
// Или, если формат ответа был 1 (текст), то РезультатЗапроса.Содержимое будет содержать текст
Иначе
Сообщение.Текст = "Ошибка при загрузке: " + РезультатЗапроса.ОписаниеОшибки;
Сообщение.Сообщить();
Возврат "";
КонецЕсли;
Мы видим, что функция ЗагрузитьСодержимоеИзИнтернет из БСП значительно упрощает код. Она самостоятельно обрабатывает перенаправления, а также предоставляет гибкие возможности для настройки запроса через структуру ДопПараметры. Это включает в себя указание метода (GET, POST), данных для отправки, дополнительных заголовков, таймаутов и работы с SSL. Использование БСП сокращает объем кода, повышает его надежность и снижает вероятность ошибок при работе с HTTP-запросами.
В этой теме мы рассмотрели распространенную проблему с HTTP-редиректом 301 при работе с HTTPСоединение в 1С. Мы выяснили, что причиной может быть такая простая деталь, как отсутствие завершающего слеша в URL директории, что заставляет сервер постоянно перенаправлять запрос.
Мы предлагаем несколько подходов к решению:
Location, контроля количества перенаправлений и адаптации метода запроса.ИнтернетПоддержкаПользователей.ЗагрузитьСодержимоеИзИнтернет из "Библиотеки стандартных подсистем". Она берет на себя всю сложность обработки редиректов и предоставляет удобный интерфейс для выполнения HTTP-запросов.Выбирая подход, всегда соизмеряйте сложность задачи с необходимостью полного контроля над процессом. В большинстве случаев БСП будет оптимальным выбором, позволяющим сэкономить время и избежать типичных ошибок. Если же требуется низкоуровневая кастомизация, будьте готовы к детальной проработке логики обработки редиректов.