Статьи

mod_rewrite: больше нет бесконечных циклов!

Глядя на журнал изменений …

Журналы изменений для Apache 2.0.45 и 1.3.28 содержат следующие примечания:

Apache 2.0.45:
Предотвратить бесконечные циклы внутренних перенаправлений в mod_rewrite
прерывание после превышения лимита внутренних перенаправлений.
ограничение по умолчанию 10 и может быть изменено с помощью RewriteOptions
директивы. PR 17462. [Андре Мало]

Apache 1.3.28:
backport из серии 2.x: предотвращение бесконечных циклов внутренних перенаправлений
в mod_rewrite путем прерывания после превышения лимита внутренних перенаправлений.
Предел по умолчанию равен 10 и может быть изменен с помощью RewriteOptions
директивы. PR 17462. [Андре Мало]

Эти два выпуска популярного веб-сервера представляют новую опцию для директивы RewriteOptions: MaxRedirects.

mod_rewrite, для тех, кто не знает, это мощный модуль, который позволяет обрабатывать входящие запросы URI и многое другое.

Эта статья объяснит использование и преимущества опции MaxRedirects. Компании веб-хостинга, которые предлагают модуль mod_rewrite, должны принять во внимание эти причины для обновления до последней версии Apache. Поддержание вашего веб-сервера в актуальном состоянии является обязательным. Хостинг-компаниям, которые не предлагают модуль mod_rewrite, рекомендуется включить его.

Прежде чем мы продолжим, вам понадобится работающая установка Apache с установленным mod_rewrite. Пожалуйста, смотрите мой FAQ для установки помощи.

Типичный случай

Давайте начнем с примера.

Мы хотим перенаправить все URI в скрипт PHP. Перенаправление будет внутренним, поэтому клиент, например браузер, не увидит, что перенаправление произошло. Для этого мы создаем в корне документа файл .htaccess, содержащий следующие правила:

# Enable rewrite engine 
Options +FollowSymLinks
RewriteEngine On

# Redirect internally all URIs to /index.php
RewriteRule .* /index.php [L]

Однако это не сработает — это замораживает ваш веб-сервер Apache. Почему?

Проблема в этой строке:

 RewriteRule .* /index.php [L]

Проще говоря, это правило «говорит»: «Перенаправляйте все запросы в /index.php.». Это может звучать так, как мы изначально стремились достичь, но на практике мы не получим ожидаемых результатов.

Вот что происходит с запросом на www.example.net/path/to/:

Фаза А

  • URI это / путь / к /
  • RewriteRule перезапишет URI из пути / в / в /index.php
  • Внутреннее перенаправление будет сделано для /index.php
  • Внутреннее перенаправление будет рассматриваться как новый запрос, поэтому он будет проанализирован снова (см. Этап b).

Фаза Б

  • Предыдущее внутреннее перенаправление, /index.php, будет снова обработано механизмом перезаписи
  • RewriteRule перепишет index.php в /index.php
  • Внутреннее перенаправление будет сделано для /index.php

В этот момент механизм перезаписи будет работать на этапе B непрерывно, а веб-сервер Apache будет зависать.

Если это звучит сложно, не волнуйтесь! Вот упрощенный файл журнала, который показывает процесс:

# phase a)  
 [] (3) [per-dir /www/htdocs/] strip per-dir prefix: /www/htdocs/path/to/ -> path/to/  
 [] (3) [per-dir /www/htdocs/] applying pattern '(.*)' to uri 'path/to/'  
 [] (2) [per-dir /www/htdocs/] rewrite path/to/ -> /index.php  
 [] (1) [per-dir /www/htdocs/] internal redirect with /index.php [INTERNAL REDIRECT]  
   
 # phase b)  
 [redir#1] (3) [per-dir /www/htdocs/] strip per-dir prefix: /www/htdocs/index.php -> index.php  
 [redir#1] (3) [per-dir /www/htdocs/] applying pattern '(.*)' to uri 'index.php'  
 [redir#1] (2) [per-dir /www/htdocs/] rewrite index.php -> /index.php  
 [redir#1] (1) [per-dir /www/htdocs/] internal redirect with /index.php [INTERNAL REDIRECT]  
   
 # phase b)  
 [redir#2] (3) [per-dir /www/htdocs/] strip per-dir prefix: /www/htdocs/index.php -> index.php  
 [redir#2] (3) [per-dir /www/htdocs/] applying pattern '(.*)' to uri 'index.php'  
 [redir#2] (2) [per-dir /www/htdocs/] rewrite index.php -> /index.php  
 [redir#2] (1) [per-dir /www/htdocs/] internal redirect with /index.php [INTERNAL REDIRECT]  
   
 # phase b)  
 [redir#3] (3) [per-dir /www/htdocs/] strip per-dir prefix: /www/htdocs/index.php -> index.php  
 [redir#3] (3) [per-dir /www/htdocs/] applying pattern '(.*)' to uri 'index.php'  
 [redir#3] (2) [per-dir /www/htdocs/] rewrite index.php -> /index.php  
 [redir#3] (1) [per-dir /www/htdocs/] internal redirect with /index.php [INTERNAL REDIRECT]  
   
 ....ad infinitum...  
   
 # phase b)  
 [redir#N] (3) [per-dir /www/htdocs/] strip per-dir prefix: /www/htdocs/index.php -> index.php  
 [redir#N] (3) [per-dir /www/htdocs/] applying pattern '(.*)' to uri 'index.php'  
 [redir#N] (2) [per-dir /www/htdocs/] rewrite index.php -> /index.php  
 [redir#N] (1) [per-dir /www/htdocs/] internal redirect with /index.php [INTERNAL REDIRECT]

Это то, что происходит с версиями Apache до версий 2.0.45 и 1.3.28.

Maxredirects

Следующий упрощенный файл журнала показывает, что произойдет, если наш пример будет работать под Apache 2.0.45 или 1.3.28:

 # phase a)  
[] (3) [per-dir /www/htdocs/] strip per-dir prefix: /www/htdocs/path/to/ -> path/to/  
[] (3) [per-dir /www/htdocs/] applying pattern '(.*)' to uri 'path/to/'  
[] (2) [per-dir /www/htdocs/] rewrite path/to/ -> /index.php  
[] (1) [per-dir /www/htdocs/] internal redirect with /index.php [INTERNAL REDIRECT]  
 
# phase b)  
[redir#1] (3) [per-dir /www/htdocs/] strip per-dir prefix: /www/htdocs/index.php -> index.php  
[redir#1] (3) [per-dir /www/htdocs/] applying pattern '(.*)' to uri 'index.php'  
[redir#1] (2) [per-dir /www/htdocs/] rewrite index.php -> /index.php  
[redir#1] (1) [per-dir /www/htdocs/] internal redirect with /index.php [INTERNAL REDIRECT]  
 
# phase b)  
[redir#2] (3) [per-dir /www/htdocs/] strip per-dir prefix: /www/htdocs/index.php -> index.php  
[redir#2] (3) [per-dir /www/htdocs/] applying pattern '(.*)' to uri 'index.php'  
[redir#2] (2) [per-dir /www/htdocs/] rewrite index.php -> /index.php  
[redir#2] (1) [per-dir /www/htdocs/] internal redirect with /index.php [INTERNAL REDIRECT]  
 
...last...  
 
# phase b)  
[redir#10] (3) [per-dir /www/htdocs/] strip per-dir prefix: /www/htdocs/index.php -> index.php  
[redir#10] (3) [per-dir /www/htdocs/] applying pattern '(.*)' to uri 'index.php'  
[redir#10] (2) [per-dir /www/htdocs/] rewrite index.php -> /index.php  
[redir#10] (1) [per-dir /www/htdocs/] internal redirect with /index.php [INTERNAL REDIRECT]

Хотя журнал перезаписи может показаться таким же, как и раньше, это не так. Внутренние перенаправления не являются непрерывными, так как они прекращаются после 10 попыток. Веб-сервер не будет зависать, и мы получим 500 HTTP-ошибку состояния (Internal Server Error).

Наш error_log будет выглядеть так:

 [Mon Jul 21 15:24:27 2003] [error] [client 127.0.0.1] mod_rewrite: maximum number of internal redirects reached. Assuming configuration error. Use 'RewriteOptions MaxRedirects' to increase the limit if neccessary.

Этот подробный журнал ошибок поможет нам быстро и точно отладить наши правила.

Несмотря на то, что мы используем те же правила, что и раньше, наш веб-сервер Apache больше не будет зависать. Разве это не волшебство?

RewriteOptions

Таким образом, наш веб-сервер больше не будет зависать … но почему внутренние перенаправления прекратились после 10 перенаправлений? Здесь вступают в действие директива RewriteOptions и опция maxredirects.

По умолчанию (то есть, если вы не объявляете это), maxredirects имеет значение:

 RewriteOptions maxredirect=10

Эта директива заставит механизм перезаписи останавливаться после 10 внутренних перенаправлений, поскольку после этого времени мы, вероятно, в бесконечном цикле.

Если число 10 перенаправлений слишком велико, вы можете изменить его на более низкое значение. Например:

 RewriteOptions maxredirect=3

Наш файл журнала будет выглядеть так:

 # phase a)   
[] (3) [per-dir /www/htdocs/] strip per-dir prefix: /www/htdocs/path/to/ -> path/to/  
[] (3) [per-dir /www/htdocs/] applying pattern '(.*)' to uri 'path/to/'  
[] (2) [per-dir /www/htdocs/] rewrite path/to/ -> /index.php  
[] (1) [per-dir /www/htdocs/] internal redirect with /index.php [INTERNAL REDIRECT]  
 
# phase b)  
[redir#1] (3) [per-dir /www/htdocs/] strip per-dir prefix: /www/htdocs/index.php -> index.php  
[redir#1] (3) [per-dir /www/htdocs/] applying pattern '(.*)' to uri 'index.php'  
[redir#1] (2) [per-dir /www/htdocs/] rewrite index.php -> /index.php  
[redir#1] (1) [per-dir /www/htdocs/] internal redirect with /index.php [INTERNAL REDIRECT]  
 
# phase b)  
[redir#3] (3) [per-dir /www/htdocs/] strip per-dir prefix: /www/htdocs/index.php -> index.php  
[redir#3] (3) [per-dir /www/htdocs/] applying pattern '(.*)' to uri 'index.php'  
[redir#3] (2) [per-dir /www/htdocs/] rewrite index.php -> /index.php  
[redir#3] (1) [per-dir /www/htdocs/] internal redirect with /index.php [INTERNAL REDIRECT]  
 
...stop

Помните, что опция maxredirect помогает контролировать преднамеренную ошибку в созданных нами правилах. Это не исправит ошибку, но остановит замораживание нашего веб-сервера.

Это хорошо как для хостинговых компаний, так и для пользователей mod_rewrite.

Выводы

И последнее, но не менее важное: что мы можем сделать, чтобы исправить правила, которые мы использовали в примере? Нам нужно использовать это правило вместо:

 RewriteRule .* index.php [L]

Разница в том, что в исправленном правиле index.php не начинается с косой черты. В последнем случае mod_rewrite достаточно умен, чтобы предотвратить бесконечные циклы, поскольку он видит, что исходный запрос index.php такой же, как внутреннее перенаправление на index.php, поэтому он не будет выполнять перенаправление. Это можно увидеть в следующем файле журнала:

 # phase a)   
[] (3) [per-dir /www/htdocs/] strip per-dir prefix: /www/htdocs/path/to/ -> path/to/  
[] (3) [per-dir /www/htdocs/] applying pattern '(.*)' to uri 'path/to/'  
[] (2) [per-dir /www/htdocs/] rewrite path/to/ -> index.php  
[] (3) [per-dir /www/htdocs/] add per-dir prefix: index.php -> /www/htdocs/index.php  
[] (2) [per-dir /www/htdocs/] strip document_root prefix: /www/htdocs/index.php -> /index.php  
[] (1) [per-dir /www/htdocs/] internal redirect with /index.php [INTERNAL REDIRECT]  
 
# phase b)  
[redir#1] (3) [per-dir /www/htdocs/] strip per-dir prefix: /www/htdocs/index.php -> index.php  
[redir#1] (3) [per-dir /www/htdocs/] applying pattern '(.*)' to uri 'index.php'  
[redir#1] (2) [per-dir /www/htdocs/] rewrite index.php -> index.php  
[redir#1] (3) [per-dir /www/htdocs/] add per-dir prefix: index.php -> /www/htdocs/index.php  
 
# Smart check that will avoid the endless loops  
[redir#1] (1) [per-dir /www/htdocs/] initial URL equal rewritten URL: /www/htdocs/index.php [IGNORING REWRITE]

Интересно, не правда ли?

Если у вас есть какие-либо вопросы, касающиеся Apache и / или mod_rewrite, посетите форум Apache SitePoint .

Список используемой литературы