Как работает mod_rewrite

mod_rewrite — модуль Apache, который переписывает URL «на лету»: красивые адреса, редиректы, склейка зеркал, блокировки, режим обслуживания. Этот туториал разбирает синтаксис по частям. Свои правила можно прогнать на конкретном URL в тестере /rewrite/, готовые блоки — в рецептах и генераторе.

Хотите пройти по шагам с упражнениями? — Академия mod_rewrite (5 уроков от первого правила до сборки целого .htaccess).

Что такое mod_rewrite и зачем он

mod_rewrite — модуль Apache, который переписывает URL запроса «на лету», до того как сервер решит, какой файл/скрипт отдавать. С его помощью делают: человекопонятные URL (ЧПУ — /blog/5 вместо /post.php?id=5); постоянные редиректы (301) при переезде/переименовании страниц; склейку зеркал (с www ↔ без www, httphttps); блокировки по User-Agent, рефереру, IP; режим обслуживания; запрет доступа к файлам. Правила пишутся в .htaccess (или в конфиге Apache).

Важно: для простых статичных редиректов mod_rewrite не обязателен — Redirect 301 /old /new и RedirectMatch из модуля mod_alias проще и не требуют RewriteEngine. mod_rewrite нужен, когда есть условия (RewriteCond), захваты, или гибкая трансформация пути. Много 301-редиректов из списка удобно сгенерировать в пакетном генераторе.

RewriteEngine On

Без строки RewriteEngine On в .htaccess все RewriteRule/RewriteCond игнорируются — это причина №1 «правила не срабатывают». Ставьте её в начало блока правил; RewriteEngine Off — наоборот, отключает. Тонкость про подкаталоги: правила (RewriteRule) из .htaccess верхнего уровня по умолчанию не действуют в .htaccess подкаталога (об этом — в разделе «частые ошибки» и про RewriteOptions Inherit); поэтому в каждом .htaccess, где есть свои rewrite-правила, RewriteEngine On принято указывать заново.

RewriteRule: паттерн → подстановка → флаги

Синтаксис: RewriteRule Паттерн Подстановка [Флаги].

  • Паттерн — регулярное выражение (PCRE). Оно матчится против пути запроса. В .htaccess в корне сайта путь подаётся без ведущего /: запрос /blog/5 → паттерн видит blog/5. (В .htaccess подкаталога — относительно этого каталога; в конфиге Apache на уровне сервера и в <VirtualHost> — с ведущим /.) Ведущий ! инвертирует паттерн.
  • Подстановка — во что превратить путь: - — не менять (полезно, когда нужно только применить флаг, напр. [F]); путь, начинающийся с /, или полный URL https://… / http://… / //host/… — см. ниже про внешний редирект; иначе — относительный путь (в .htaccess подкаталога — относительно RewriteBase). В подстановке доступны бэкрефы $1, $2… (захваты групп паттерна; $0 — весь матч), %1, %2… (захваты последнего RewriteCond), %{ИМЯ} (серверная переменная).
  • Флаги — в квадратных скобках через запятую: [R=301,L]. См. следующий раздел.

Пример — ЧПУ для блога (/blog/5 обрабатывается так, будто пришёл запрос /post.php?id=5):

.htaccessкопировать
RewriteEngine On
RewriteRule ^blog/(\d+)$ post.php?id=$1 [L]

^blog/(\d+)$ — «строка начинается с blog/, дальше одна или больше цифр (захват в группу 1), и больше ничего»; $1 в подстановке = эти цифры.

Внутренняя перезапись vs внешний редирект — ключевое различие. Внутренняя перезапись: путь меняется только внутри сервера, браузер ничего не видит, адрес в строке не меняется — так работает RewriteRule . index.php [L] (фронт-контроллер) или пример выше. Внешний редирект: сервер отвечает кодом 30x и Location:, браузер переходит по новому адресу — так работает RewriteRule с флагом [R] (или [R=301] и т. п.), либо если подстановка — полный URL со схемой (https://…). Пример внешнего редиректа:

.htaccessкопировать
RewriteEngine On
RewriteRule ^old-page$ /new-page [R=301,L]

Флаги RewriteRule

  • L — «last»: остановить обработку правил в этом проходе. Но в .htaccess (per-directory контекст) Apache 2.4 после любой перезаписи перезапускает весь набор правил с начала — и так, пока URL не перестанет меняться. Поэтому [L] ≠ «больше ничего не делать никогда». Для окончательного «стоп» — END.
  • END — окончательно остановить обработку и не перезапускать набор (Apache 2.4+).
  • R / R=код — внешний редирект. Без значения — 302 (временный). Для постоянного редиректа — R=301. Можно R=302, R=303, R=307; для блокировок — F (даёт 403) и G (410) обычно понятнее.
  • NC — «nocase»: матчить паттерн (и в RewriteCondCondPattern) без учёта регистра.
  • QSA — «query string append»: присоединить исходную строку запроса (?…) к новой. Без QSA query из подстановки заменяет исходный; если в подстановке ? нет — исходный query сохраняется как есть.
  • QSD — «query string discard»: отбросить строку запроса (Apache 2.4+).
  • F — «forbidden»: вернуть 403 Forbidden (подстановку обычно пишут -).
  • G — «gone»: вернуть 410 Gone.
  • C / chain — «chain»: следующее правило выполнится, только если это сработало. Если это правило не сматчилось — следующее (и дальше по цепочке) пропускается.
  • S=N / skip=N — при срабатывании правила пропустить следующие N правил (что-то вроде goto).
  • NE — «noescape»: не URL-кодировать спецсимволы в подстановке (?, #, % и т. п. останутся как есть).
  • E=ИМЯ:ЗНАЧЕНИЕ — выставить переменную окружения (доступна потом как %{ENV:ИМЯ} и в скриптах как $_SERVER['ИМЯ']).
  • N / next — перезапустить обработку набора правил с начала немедленно (а не после прохода). Легко получить бесконечный цикл — используйте осторожно.
  • P / proxy — проксировать запрос через mod_proxy (вместо редиректа).
  • NS (nosubreq), T=тип (type), B, DPI (discardpath), PT (passthrough), H=обработчик (handler) — реже; см. документацию Apache.

RewriteCond: условия перед правилом

Синтаксис: RewriteCond ТестоваяСтрока ШаблонУсловия [Флаги]. RewriteCond ставится перед RewriteRule и относится к ближайшему следующему RewriteRule. Несколько RewriteCond подряд — все должны быть истинны (логическое И). Флаг [OR] на условии объединяет его со следующим в группу-«ИЛИ»: RewriteCond A [OR] + RewriteCond B → правило сработает, если A или B; группы между собой по-прежнему через И.

  • ТестоваяСтрока — что проверяем; обычно серверная переменная (%{HTTP_HOST}), можно с бэкрефами %N/$N и текстом.
  • ШаблонУсловия — это: регулярное выражение (PCRE) — истинно, если ТестоваяСтрока ему соответствует; лексикографические сравнения <строка (меньше), >строка (больше), =строка (точно равна), <=строка, >=строка (Apache 2.4); числовые сравнения (Apache 2.4) -eq, -ne, -lt, -le, -gt, -ge (обе стороны как целые); проверки файловой системы -f (существующий обычный файл), -d (каталог), -l (символьная ссылка), -s (непустой файл), -x (исполняемый) — обычно ТестоваяСтрока тут %{REQUEST_FILENAME}; ведущий ! перед любым из перечисленного — инверсия; флаги [NC] (без учёта регистра), [OR].

Классический пример — «фронт-контроллер»: всё, что не существующий файл и не существующий каталог, отдать в index.php:

.htaccessкопировать
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . index.php [L]

(%{REQUEST_FILENAME} — полный путь к файлу на диске, который соответствовал бы запросу; !-f — «это не файл», !-d — «это не каталог»; паттерн . матчит любой непустой путь.)

Пример с [OR] — редирект и example.com, и www.example.com на https://example.com:

.htaccessкопировать
RewriteEngine On
RewriteCond %{HTTP_HOST} ^(www\.)?example\.com$ [NC]
RewriteCond %{HTTPS} off [OR]
RewriteCond %{HTTP_HOST} ^www\. [NC]
RewriteRule ^ https://example.com%{REQUEST_URI} [R=301,L]

(Первое условие ограничивает домен; затем «не-HTTPS или с www» → редиректим на канонический https://example.com.)

Переменные %{…}

  • %{HTTP_HOST} / %{SERVER_NAME} — хост из запроса (example.com).
  • %{HTTPS}on, если запрос по HTTPS, иначе off. (За reverse-proxy/CDN может всегда быть off — тогда смотрите %{HTTP:X-Forwarded-Proto}.)
  • %{REQUEST_SCHEME}http или https.
  • %{SERVER_PORT} — порт (80, 443, …).
  • %{REQUEST_URI} — путь запроса (/blog/5), без строки запроса. Меняется при внутренней перезаписи.
  • %{QUERY_STRING} — то, что после ? (без ?).
  • %{REQUEST_METHOD}GET, POST, HEAD, …
  • %{REQUEST_FILENAME} / %{SCRIPT_FILENAME} — полный путь к файлу на диске, соответствующий запросу (= DOCUMENT_ROOT + путь); используется в проверках -f/-d.
  • %{DOCUMENT_ROOT} — корневой каталог сайта на диске.
  • %{THE_REQUEST} — полная первая строка HTTP-запроса (GET /blog/5?x=1 HTTP/1.1). Не меняется при внутренних перезаписях — поэтому используется в правилах-редиректах, чтобы не зациклиться (см. «частые ошибки»).
  • %{HTTP_USER_AGENT} — строка User-Agent клиента (для блокировки ботов).
  • %{HTTP_REFERER} — заголовок Referer (для защиты от хотлинка / блокировки трафика).
  • %{HTTP_COOKIE} — заголовок Cookie.
  • %{HTTP:Имя-Заголовка}любой входящий HTTP-заголовок, напр. %{HTTP:X-Forwarded-Proto}, %{HTTP:Accept-Language}.
  • %{ENV:ИМЯ} — переменная окружения (например, выставленная флагом [E=…] или директивой SetEnvIf).
  • %{REMOTE_ADDR} — IP-адрес клиента.
  • %{TIME}, %{TIME_YEAR}, %{TIME_MON}, %{TIME_DAY}, %{TIME_HOUR}, %{TIME_MIN}, %{TIME_SEC}, %{TIME_WDAY} — текущее время сервера (TIMEГГГГММДДЧЧММСС).
  • %{IS_SUBREQ}true, если это внутренний под-запрос (например, при Include/негоциации), иначе false.

Запомните разницу: %{REQUEST_URI} меняется при внутренней перезаписи, %{THE_REQUEST}нет. Поэтому в правиле-редиректе, который должен сработать только на «оригинальный» URL, проверяют %{THE_REQUEST}.

Бэкрефы: $N (правило) и %N (условие)

$1$9 — то, что захватили группы ( … ) в паттерне последнего сматчившего RewriteRule; $0 — весь матч. Используются в подстановке этого RewriteRule.

%1%9 — захваты последнего сматчившего RewriteCond-паттерна; %0 — весь матч. Используются и в ТестовойСтроке последующих RewriteCond той же цепочки, и в подстановке RewriteRule.

Пример с обоими — редирект с www на без www, сохраняя путь:

.htaccessкопировать
RewriteEngine On
RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC]
RewriteRule ^(.*)$ https://%1/$1 [R=301,L]

Здесь %1 = всё после www. (домен без www.), $1 = путь запроса. (Если у сайта нет TLS — поставьте http:// вместо https://.)

RewriteBase

RewriteBase нужна для .htaccess в подкаталоге и только для относительных подстановок: она говорит mod_rewrite, какой URL-префикс соответствует этому каталогу, чтобы правильно построить итоговый URL.

Пример — .htaccess лежит в /app/, доступном как https://example.com/app/:

.htaccessкопировать
RewriteEngine On
RewriteBase /app/
RewriteRule ^login$ login.php [L]

Запрос /app/login обрабатывается как /app/login.php. Без RewriteBase в подкаталоге относительная подстановка login.php может «уехать» (mod_rewrite попытается угадать базу по физическому пути).

Когда RewriteBase не нужна: если это .htaccess в корне сайта; или если все ваши подстановки абсолютные (начинаются с / — относительно DocumentRoot) либо это полные URL. Многие современные конфиги пишут подстановки абсолютными именно чтобы не зависеть от RewriteBase.

Порядок обработки и [L]/[END] в .htaccess

Правила в .htaccess обрабатываются сверху вниз. Каждое подходящее RewriteRule меняет текущий URL; затем, если не было [L], обработка продолжается со следующего правила — уже с новым URL. Поэтому порядок правил важен, и без [L] правила могут «накладываться».

Тонкость .htaccess-контекста (per-directory): в Apache 2.4 после того как набор правил отработал и URL изменился, mod_rewrite запускает весь набор заново — и так до тех пор, пока проход не перестанет менять URL (или не наберётся ~10 итераций — тогда 500). Поэтому [L] останавливает только текущий проход, а не «всю обработку навсегда». Если нужно действительно остановиться и не перезапускать набор — флаг [END] (Apache 2.4+). Непонимание этого — частый источник лупов и неожиданного поведения.

Ещё раз про результат правила: внутренняя перезапись (RewriteRule . index.php [L]) — браузер ничего не видит, адрес не меняется; внешний редирект ([R], [R=301] или подстановка с полным URL) — браузер получает 30x и меняет адрес. Не путайте: если хотели «красивый URL без редиректа», флага [R] быть не должно.

Частые ошибки и грабли

  • Нет RewriteEngine On — правила молча игнорируются. Проверьте, что эта строка есть и стоит до правил (в каждом .htaccess, где есть свои правила).
  • 500 Internal Server Error, в логе «Invalid command 'RewriteRule'»mod_rewrite не включён на сервере/хостинге. Включите модуль (или попросите хостинг), либо оберните правила в <IfModule mod_rewrite.c> … </IfModule>, чтобы хотя бы не падало.
  • Относительная подстановка с [R]RewriteRule ^old$ new-page [R=301] приведёт к редиректу на адрес, достроенный через DocumentRoot (часто не туда). Указывайте абсолютный путь /new-page или полный URL https://example.com/new-page. (Проверить, во что превратится URL, можно в тестере /rewrite/.)
  • Бесконечный редирект — ERR_TOO_MANY_REDIRECTS — обычно: правило-редирект проверяет %{REQUEST_URI} (который меняется при перезаписи) вместо %{THE_REQUEST} (который нет); либо редирект ведёт на путь, который сам же снова попадает под правило. Используйте %{THE_REQUEST} для проверки «оригинального» URL и убедитесь, что цель редиректа под правило больше не подпадает.
  • Забыли [L] — правила «текут» дальше: следующее правило применяется к уже переписанному URL и накладывает ещё одну трансформацию. Ставьте [L] там, где обработка должна остановиться.
  • Не экранировали . в паттерне — точка в регулярке означает «любой символ»: example.com матчит и exampleXcom, и example-com. Если нужна буквальная точка — example\.com. То же для ?, +, (, ), [, ], *, ^, $ и т. п. — в паттерне это спецсимволы.
  • .htaccess в подкаталоге не наследует правила родителя — по умолчанию RewriteRule из .htaccess верхнего уровня не применяются в .htaccess подкаталога; нужно RewriteOptions InheritDown в родительском .htaccess (или RewriteOptions InheritDownBefore), либо RewriteOptions Inherit в дочернем.
  • Путать перезапись и редиректRewriteRule . index.php [L] НЕ меняет адрес в браузере (это внутренняя перезапись), а RewriteRule ^x$ /y [R=301,L] — меняет. Если адрес «не меняется, а должен» — добавьте [R]; если «меняется, а не должен» — уберите [R].
  • 404 после включения ЧПУ, бесконечный редирект, правило не срабатывает — типовые причины и их исправление разобраны в гиде «Почему .htaccess не работает».
  • Сомневаетесь в самой регулярке (что значит [^/], \., (.*), как нумеруются $1/$2) — разбор синтаксиса PCRE в справочнике «PCRE: регулярные выражения для mod_rewrite».

Проверить и собрать

Тестер RewriteRule — прогнать свои правила на конкретном URL (пошаговая трассировка: какое правило сработало, какие RewriteCond прошли, во что превратился адрес, итог). Тестер регулярных выражений — проверить отдельный regex-паттерн на примерах строк, увидеть захваты $1/$2 и разбор паттерна. Объяснялка .htaccess — построчный разбор файла, включая флаги RewriteRule и распознавание типовых блоков. Линтер проверка .htaccess — синтаксис файла, баланс блоков, устаревшие директивы. Генератор .htaccess — блоки на mod_rewrite (редиректы, HTTPS, блокировка ботов/хотлинка, режим обслуживания) собираются чекбоксами. Пакетный генератор 301-редиректов — много 301 из списка «старый URL → новый URL». Рецепты .htaccess — готовые блоки под типовые задачи. Справочник директив .htaccess — что делает каждая директива, синтаксис, пример. Статьи: блокировка по referer, фильтрация по IP. «.htaccess и nginx» — таблица соответствий и почему nginx не читает .htaccess.