Почему mod_rewrite это вуду, и в каком на самом деле порядке выполняются правила RewriteRule

Сегодня мне пришлось составлять довольно хитрый редирект: если пользователь запросил /news/some-section/, то его нужно было перенаправить на просто /section/, но потом всё равно сделать внутренний редирект на /news/some-section/, так как сам движок новостей лежит именно там.

Я написал простое правило, обновил страничку… и получил от mod_rewrite по полной программе, потому что он ушёл в бесконечный цикл и не собирался из него возвращаться. Я открыл документацию и начал разбираться. Потом включил RewriteLog. Сначала я был озадачен. Потом стал откровенно злиться. А к тому моменту, когда задача была решена, я успел пережить такое количество разных чувств, что кроме равнодушного вздоха ничем больше не смог отметить достигнутый результат.

Оказывается, mod_rewrite называют вуду не потому, что он волшебный. А потому что для того, чтобы осознать, как он работает, нужно три раза прочитать по нему документацию. Вслух. С выражением! Потом найти все параграфы, начинающиеся со слова «Note» и пересказать их своими словами. Потом нужно найти все параграфы, в которых больше одного предложения и тоже пересказать их своими словами. Тоже вслух. И с выражением тоже.

Но и это ещё не всё. После этого нужно пойти по ссылке «detailed mod_rewrite documentation» и почитать там. А потом отправиться впитывать полезный опыт в гугл.

Так много слов выше написано оттого, что сам я только что всё это проделал и мне нужно высказаться. Вы, наверное, думаете — ну и где же обещанное «как на самом деле»? Подождите, сейчас расскажу.

По работе мне приходится составлять довольно простые правила, зато их довольно много. Приходится следить, чтобы они не пересекались друг с другом. Директив использую немного — RewriteCond, RewriteRule и флаги [L], [R], изредка [NC] и [QSA]. Без усложний с map’ами, chain’ами и subreq’ами.

Опыт программирования подсказывал мне (и некоторые другие программисты были со мной согласны), что если я сказал [R], то в этом месте должен начаться редирект. А если указано [L], то разбор правил должен прекратиться (совсем-совсем прекратиться). Ну и когда все правила пройдены, разбор прекращается, отдаётся скрипт, на который смотрит получившийся адрес.

Ага. Щаз.

Оказывается, всё работает несколько иначе.

[R] на самом деле не делает редирект. Он просто делает текущий адрес абсолютным (дописывает к нему http://хост/) и где-то себе запоминает, что потом нужно будет сделать внешний редирект. После этого разбор правил продолжается до конца .htaccess, и вот уже тут происходит перенаправление.

[L] на самом деле не останавливает полностью разбор правил.
Если разбор происходит в .htaccess (как это обычно бывает), то совершается переход в конец .htaccess, и вот тут уже… происходит перенаправление? Ну да, только в его результате мы с большой вероятностью опять попадаем на тот же .htaccess, и начинается проход по всем правилам заново (даже при внутреннем редиректе!). И этот цикл будет продолжаться до тех пор, пока не настанет момент, когда ни одно правило не сработает (или пока мы редиректом не выйдем из директории, в которой лежит наш .htaccess). Вот тогда уже сервер отдаст страничку.

Единственно предсказуемое поведение дают [R,L] вместе взятые. В этом случае отмечается, что должен произойти редирект, пропускаются все следующие правила и юзеру отдаётся перенаправление на указанную страницу.

Какой из этого вывод? Не надо везде подряд ставить [L]. Если, конечно, хотите понимать, в каком порядке обрабатываются правила. Если [L] нигде не указан, то проход осуществляется строго подряд. Правда, когда правила закончатся, и окажется, что какое-то из них поменяло ссылку, всё равно будет запущен проход по всем правилам ещё раз. Но это уже гораздо проще контролировать.

Например. Обычно ссылка после изменения не подходит под то же или под какое-либо другое правило. То есть, если вы перенаправляете /news в index.php/news:

RewriteRule ^news/$ index.php/news

то второй раз это правило уже не сработает.

Однако в моём случае нужно было запрос пользователя /news/section-name/ перенаправлять в /section-name/, а потом делать внутренний редирект обратно с /section-name/ на /news/section-name/, где находился обработчик новостей. То есть:

RewriteRule ^news/section-name/$ section-name/ [R=301,L]
RewriteRule ^section-name/ news/section-name/

Первый редирект внешний, второй внутренний. Если бы всё происходило в один проход, всё было бы нормально. Но так как проходы длятся пока срабатывает хотя бы одно правило, происходило следующее. Пользователь набрал /news/section-name/. Его внешним редиректом перенаправило на /section-name/. Потом сработало второе правило и произошёл внутренний редирект на news/section-name/. Потом mod_rewrite обнаружил, что ссылка изменилась, и инициировал повторную обработку запроса. Опять сработало первое правило, пользователь получил внешний редирект. Круг замкнулся.

Для того, чтобы разрешить внешний редирект только в том случае, если запрос /news/section-name/ прислан пользователем, а не сгенерирован RewriteRule, приходится проверять %{THE_REQUEST}:

RewriteCond %{THE_REQUEST} ^(GET|HEAD)\ /news/section-name/
RewriteRule ^news/(.*) /$1 [R=301,L]

Работает: после внешнего редиректа пользователь запрашивает /section-name/, этот запрос попадает в THE_REQUEST и для первого правила не выполняется RewriteCond.

За информацию спасибо следующим ссылкам:

* http://httpd.apache.org/docs/2.2/mod/mod_rewrite.html
* http://www.askapache.com/htaccess/mod_rewrite-basic-examples.html
* http://httpd.apache.org/docs/2.2/rewrite/rewrite_tech.html
* http://www.google.com

Там написано, почему при обработке правил в .htaccess происходят повторные запросы с повторным проходом по всем правилам, и почему при указании директив редиректа в <VirtualHost> или в глобальном конфиге Apache не в разделе <Directory> проход происходит только один раз.

комментария 2

  • Мда, заколебался я с этим [L]
    Получается надо составлять ссылки и правила, что они повторно не срабатывали.

  • Спасибо за статью! Передо мной стояла такая же задача — сделать одновременно и прямое и обратное преобразование. [L] не работает.
    Пробовал через использование переменных:
    RewriteRule ^section-name/ news/section-name/ [E=my_flag:1,L]
    RewriteCond %{ENV:my_flag} !1
    RewriteRule ^news/(.*) /$1 [R=permanent,E=my_flag:1]
    И опять не работает! Не сохраняются значение переменной. Не знаю почему.

    А вот метод с %{THE_REQUEST} прекрасно заработал.

Добавить комментарий