Почему HTTPСоединение в 1С возвращает 301 редирект и как его победить?

Программист 1С v8.3 (Управляемые формы)
← На главную

Когда мы работаем с внешними веб-ресурсами из 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: что означают редиректы?

Прежде чем углубляться в решение, давайте вспомним, что означают коды состояния HTTP в диапазоне 3xx. Эти коды не являются ошибками (как, например, 4xx или 5xx), а указывают на необходимость выполнения клиентом дальнейших действий для завершения запроса. Они используются сервером для перенаправления запроса на другой адрес.

  1. 301 Moved Permanently (Перемещено окончательно): Этот код сообщает, что запрошенный ресурс окончательно перемещен на новый URI. Клиент должен использовать этот новый URI для всех будущих запросов к данному ресурсу. Важно отметить, что браузеры и некоторые клиенты могут кэшировать это перенаправление.
  2. 302 Found (ранее Moved Temporarily - Найдено, Перемещено временно): Ресурс временно доступен по другому URI. Клиент должен продолжать использовать исходный URI для будущих запросов, так как ресурс может вернуться на старое место.
  3. 303 See Other (Смотрите другое): Этот код часто используется после запроса POST. Он указывает, что ответ на запрос может быть найден по другому URI и должен быть получен с использованием метода GET, независимо от исходного метода запроса.
  4. 307 Temporary Redirect (Временное перенаправление): Подобно 302, ресурс временно доступен по другому URI. Однако, ключевое отличие состоит в том, что метод запроса (например, POST) не должен изменяться при перенаправлении.
  5. 308 Permanent Redirect (Постоянное перенаправление): Аналогично 301, ресурс окончательно перемещен. Главное отличие от 301 заключается в том, что метод запроса не должен изменяться при перенаправлении, аналогично 307.

В исходном коде используется условие Результат.КодСостояния >= 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-редиректов в 1С

Хотя добавление слеша решило конкретную проблему, в более общих случаях вам может потребоваться более универсальный механизм для следования по редиректам. Объекты HTTPСоединение и HTTPЗапрос в 1С не выполняют автоматическое следование по редиректам, поэтому разработчику необходимо реализовать эту логику самостоятельно. Давайте рассмотрим шаги.

  1. Ограничение количества редиректов: Чтобы избежать бесконечного цикла, всегда устанавливайте ограничение на максимальное количество перенаправлений. Например, 5 или 10 попыток.

    
    МаксРедиректов = 5;
    ТекущийРедирект = 0;
    // ...
    Пока (Результат.КодСостояния >= 300 И Результат.КодСостояния < 400) И (ТекущийРедирект < МаксРедиректов) Цикл
        ТекущийРедирект = ТекущийРедирект + 1;
        // ... логика обработки редиректа ...
    КонецЦикла;
    
  2. Корректный парсинг заголовка 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 для нового запроса.

  3. Учет изменения метода запроса: Для некоторых кодов редиректа, таких как 303 See Other, метод запроса должен измениться на GET, даже если исходный запрос был POST. Для 307 Temporary Redirect и 308 Permanent Redirect метод запроса должен оставаться прежним.

    
    МетодЗапроса = "GET"; // Исходный метод
    Если Результат.КодСостояния = 303 Тогда
        МетодЗапроса = "GET";
    ИначеЕсли Результат.КодСостояния = 307 ИЛИ Результат.КодСостояния = 308 Тогда
        // МетодЗапроса остается прежним
    КонецЕсли;
    // Далее ВызватьHTTPМетод(МетодЗапроса, ...)
    

    Это требует использования метода ВызватьHTTPМетод вместо Получить или ОтправитьДляОбработки для большей гибкости.

  4. Передача куки (Cookies): Если первый запрос получил куки (через заголовок Set-Cookie), и эти куки необходимы для последующего перенаправленного запроса (например, для поддержания сессии), их необходимо извлечь и передать в заголовке Cookie нового запроса.

    
    // Получение куки из первого ответа
    КукиСервера = Результат.Заголовки["Set-Cookie"]; // Если есть
    // ... обработка и формирование заголовка "Cookie" для следующего запроса
    ЗаголовкиДляНовогоЗапроса.Вставить("Cookie", "имя_куки=значение_куки; ...");
    
  5. Настройка защищенного соединения: Если редирект происходит на 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 директории, что заставляет сервер постоянно перенаправлять запрос.

Мы предлагаем несколько подходов к решению:

  1. Простое решение: Всегда убеждайтесь, что URL для запроса к директории завершается слешем. Это может предотвратить многие ненужные редиректы.
  2. Ручная универсальная обработка: Если вы сталкиваетесь с более сложными сценариями редиректов (множественные перенаправления, изменение протокола, специфичные куки или методы запроса), вам придется реализовать полноценный механизм ручной обработки, учитывающий все нюансы, описанные выше. Это требует тщательного парсинга заголовка Location, контроля количества перенаправлений и адаптации метода запроса.
  3. Решение с использованием БСП: Для большинства практических задач наиболее эффективным и надежным способом является использование функции ИнтернетПоддержкаПользователей.ЗагрузитьСодержимоеИзИнтернет из "Библиотеки стандартных подсистем". Она берет на себя всю сложность обработки редиректов и предоставляет удобный интерфейс для выполнения HTTP-запросов.

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

← На главную