SEO попросил 301 со старого ЧПУ раздела, а в init.php скопировали сниппет с форума: CHTTP::SetStatus и LocalRedirect со строкой статуса. Через неделю в Метрике старый URL жив, curl -I пишет 302 Found. Речь про CMS 1С-Битрикс: Управление сайтом, не Bitrix24. Постоянный hop даёт localredirect битрикс только третьим параметром; ниже вызов, ловушка аргументов и проверка без петли.
LocalRedirect – это функция главного модуля CMS "1С-Битрикс: Управление сайтом": она нормализует URL, вызывает OnBeforeLocalRedirect, выставляет HTTP-статус через CHTTP::SetStatus (по умолчанию 302 Found) и заголовок Location, затем завершает хит. Постоянный 301 задают только третьим параметром с версии 8.5.1; второй параметр – skip_security_check, не код ответа. Не-3xx статусы справка игнорирует и снова отдаёт 302. Сегодня grep-ните вызовы без false, поставьте "301 Moved Permanently" третьим параметром и снимите curl -I.
На практике коллега "починил" дефолт в bitrix/modules/main/tools.php – после обновления главного модуля снова 302. Защита редиректов ругается: строка во втором аргументе truthy и включает skip phishing. Нужна одна строка: LocalRedirect($RESURL, false, "301 Moved Permanently").
Выберите слой: PHP, SEO-модуль или сервер
Типичная ошибка – писать в PHP сотни статичных пар и параллельно крутить те же URL в админке. Массовые склейки после смены ЧПУ – в SEO-модуле и на сервере, это гайд про настройку 301 редиректов на Битрикс. Сюда заходите, когда адрес зависит от роли, свойства, пустого раздела или пагинации.
| Когда | Куда | Не делать |
|---|---|---|
| Адрес зависит от роли, свойства, пустого раздела, пагинации | PHP: LocalRedirect или D7 Redirect | Дублировать те же URL в админке |
| Сотни статичных пар old→new после смены ЧПУ | SEO-модуль в админке | Писать каждую пару в init.php |
| https, www, слеш, тысячи правил до PHP | .htaccess или nginx выше urlrewrite | Собирать http через IsHTTPS() |
| Страницы больше нет, аналога нет | 404 через CHTTP::SetStatus, не 301 на /404 | 301 на /404.php вместо настоящего 404 |
| Логин, POST-форма, временная акция | 302 или 303 | 301: браузер закэширует hop |
Вердикт: PHP – когда условие из кода или базы. Статичные пары и доменные склейки – другой слой. ЧПУ urlrewrite не заменяет HTTP 301: это маршрутизация, не "страница навсегда переехала".
Делайте: один слой на пару URL. Не делайте: init.php "на все редиректы", пока не проверили SEO-модуль.
Вызовите LocalRedirect с третьим параметром 301
Как сделать редирект из PHP Битрикс с кодом 301: статус идёт третьим аргументом. Сигнатура с 8.5.1: url, skip_security_check=false, status="302 Found". skip_security_check с 8.0.3 – это не код ответа, а обход защиты от фишинга.
LocalRedirect($RESURL, false, "301 Moved Permanently");
В status только коды 3xx. Строка "404 Not Found" третьим аргументом игнорируется – снова 302. Для постоянной склейки GET нужен "301 Moved Permanently". 302 оставьте логину и акциям.
Схема вызова:
условие в init.php → LocalRedirect($url, false, "301 Moved Permanently") → ядро ставит SetStatus и Location → curl -I: один 301
- Найдите все LocalRedirect в init.php, компоненте и обработчике.
- Поставьте второй аргумент строго false, не строку статуса.
- Третьим аргументом передайте 301 Moved Permanently.
- Не вызывайте SetStatus перед hop: функция сделает это сама.
- Проверьте URL: относительный путь безопаснее http, собранного вручную.
- Оставьте skip_security_check=false, иначе откроете внешний редирект.
Делайте: одну строку с тремя аргументами. Не делайте: надеяться, что дефолт "сам станет 301", если SEO "очень просил".
Уберите антипаттерн SetStatus и строку вместо false
Живой пример с форума (пост 10529, фикс 2017): CHTTP::SetStatus("301 Moved Permanently"); LocalRedirect($RESURL, "301 Moved Permanently");. В DevTools и curl всё равно 302 Found. Поисковик не склеивает URL, хотя "редирект уже есть".
LocalRedirect сама вызывает CHTTP::SetStatus($status) перед header("Location") и завершает хит. Предварительный SetStatus – мёртвый код: ядро перезапишет статус третьим параметром, без него подставит "302 Found". Второй аргумент-строка – skip_security_check: непустая строка truthy, защита отключается, статус остаётся дефолтным.
// антипаттерн: снова 302, плюс skip phishing
CHTTP::SetStatus("301 Moved Permanently");
LocalRedirect($RESURL, "301 Moved Permanently");
В реальном проекте правили дефолт в tools.php. После обновления главного модуля hop снова 302. SetStatus – для 404 без Location, не для переезда; это настройка 404 на Битрикс.
Делайте: grep по LocalRedirect($url, "301 и по SetStatus прямо перед hop. Не делайте: патчить bitrix/modules/main/tools.php.
Поставьте 301 через D7 setStatus, не через skipSecurity
В контроллере берите \Bitrix\Main\Engine\Response\Redirect. Дефолт тоже 302. Постоянный код задаёт setStatus, не конструктор. Не копируйте community-пост "с какого релиза появился класс": в выдаче он уже 404, официальная D7-справка версии не ставит.
$response = new \Bitrix\Main\Engine\Response\Redirect($url);
$response->setStatus("301 Moved Permanently");
$response->send();
Ответ можно вернуть из действия без ручного send. Чтобы оборвать хит, после send вызывают Application::end. redirectTo и setSkipSecurity есть в class reference; skipSecurity=true не ставьте: это тот же обход защиты, что второй аргумент LocalRedirect.
- Создайте Redirect с целевым путём.
- Вызовите setStatus со строкой 301 Moved Permanently.
- Верните объект из контроллера или вызовите send и end.
- Не копируйте skipSecurity=true из чужого gist.
Результат тот же: один hop 301 и Location. Получите предсказуемый D7-код без правки ядра. Делайте: setStatus рядом с Redirect. Не делайте: смешивать LocalRedirect и send на одном хите.
Проверьте композит, событие OnBeforeLocalRedirect и петлю HTTPS
События OnBeforeLocalRedirect и OnLocalRedirect есть с 6.5.4. Обработчик может подменить URL до Location. Если после деплоя hop уезжает "не туда", grep по AddEventHandler("main", "OnBeforeLocalRedirect". Часто ломается не ваш вызов, а чужой обработчик.
На композите редирект во время AJAX даёт X-Bitrix-Composite: Ajax (error:redirect). Динамика не подгружается. Не редиректите frame-хит: обычный GET должен отдать 301 без error. Включать композит с нуля не нужно; hop – в гайде про композитный сайт на Битрикс.
После SSL за прокси PHP часто видит HTTP. LocalRedirect через CMain::IsHTTPS() собирает http://, nginx кидает на https, браузер пишет ERR_TOO_MANY_REDIRECTS. Лечится X-Forwarded-Proto и https в .settings.php, не третьим 301 "поверх". Тест "внутреннее перенаправление" ждёт 302 Location; Cloudflare может сломать check_localredirect. Это не повод менять SEO-статус.
Проверьте curl -I обычным GET: Location и отсутствие Ajax (error:redirect). Не делайте: дублировать HTTPS-склейку в PHP, если её уже делает nginx.
Закройте чек-лист: curl, корзина и обмен 1С
Критерий готовности: один hop 301 Moved Permanently и Location, без 302 на повторном curl -I без -L. Корзина, /personal/ и 1c_exchange.php остаются 200. На композите нет Ajax (error:redirect). В коде нет LocalRedirect($url, "301 без false.
- Запустите curl -I на старый путь: статус 301, не 302 Found.
- Сверьте заголовок Location с каноническим URL, без лишнего слеша.
- Прогоните curl -I -L --max-redirs 5: не больше одного hop, цель 200.
- Откройте корзину и /personal/: не должно уехать 301.
- Проверьте ночной обмен: 1c_exchange.php отвечает 200, не редиректом.
- Исключите POST-логин: на авторизацию ставьте 302 или 303, 301 кэшируется.
- Сбросьте композитный кеш страницы, если видели error:redirect.
$path = $APPLICATION->GetCurPage(false);
if (strpos($path, "/bitrix/admin/1c_exchange.php") === 0
|| strpos($path, "/personal/") === 0
|| strpos($path, "/basket/") === 0) {
return;
}
Сможете закрыть задачу SEO без сюрприза утром: обмен жив, корзина открывается, hop постоянный. Если условие раскидано по ролям и легаси-обработчикам, обсудите правку на конкретном init.php, а не копируйте ещё один сниппет.
Делайте: стоп-лист служебных путей до LocalRedirect. Не делайте: 301 на POST и на URL, которые должны остаться 200.
Автор: Максим Мольков, разработчик 1С-Битрикс.
Источники: справка LocalRedirect, D7 Redirect::setStatus, MDN 301, композит error:redirect.
Частые вопросы
Чем LocalRedirect отличается от редиректа в модуле SEO?
LocalRedirect срабатывает на хите, когда условие из PHP или базы. Модуль SEO хранит массовые статичные пары без деплоя. Много пар без роли – в админку, не в init.php.
Нужно ли вызывать CHTTP::SetStatus перед LocalRedirect?
Нет. Функция сама вызывает CHTTP::SetStatus($status) перед Location. Предварительный SetStatus ядро перезапишет. SetStatus оставляйте для 404 без Location.
Почему LocalRedirect отдаёт 302?
Дефолт функции – "302 Found". Второй аргумент-строка не статус, а skip_security_check. Не-3xx справка игнорирует и снова ставит 302. Передайте false и "301 Moved Permanently" третьим параметром.
Как проверить 301 на композитном сайте?
Снимите curl -I обычным GET, не AJAX-frame. Нужны 301 и Location. Заголовка X-Bitrix-Composite: Ajax (error:redirect) быть не должно. Затем сбросьте композитный кеш.
Можно ли ставить 301 на авторизацию и POST-формы?
Нет. 301 кэшируется, логин и POST "залипнут". Для входа используйте 302 или 303. 301 – постоянному переезду GET-страницы.
LocalRedirect не работает в проверке системы. Это не 301?
Тест check_localredirect ждёт 302 Found и Location. Cloudflare и прокси ломают hop. Чините инфраструктуру, не SEO-статус. skip_security_check и setSkipSecurity к тесту не привязывайте.
Было полезно?
Поставьте лайк, если статья помогла разобраться.
Поделиться статьёй
Отправьте тому, кому это пригодится.
Можно поделиться и фрагментом: выделите текст статьи.