调试.htaccess重写规则的技巧

308
许多海报在其.htaccess文件中调试RewriteRule和RewriteCond语句时会遇到问题。其中大部分人使用共享托管服务,因此无法访问根服务器配置。他们不能避免使用.htaccess文件进行重写,并且像许多回答者建议的那样,无法启用“RewriteLogLevel”。此外,有许多.htaccess特定的陷阱和限制没有得到很好的涵盖。对于大多数人来说,设置本地测试LAMP堆栈涉及太多的学习曲线。

因此,我的问题是如何建议他们自己调试规则。我在下面提供了一些建议。欢迎提出其他建议。

  1. Understand that the mod_rewrite engine cycles through .htaccess files. The engine runs this loop:

    do
      execute server and vhost rewrites (in the Apache Virtual Host Config)
      find the lowest "Per Dir" .htaccess file on the file path with rewrites enabled
      if found(.htaccess)
         execute .htaccess rewrites (in the user's directory)
    while rewrite occurred
    

    So your rules will get executed repeatedly and if you change the URI path then it may end up executing other .htaccessfiles if they exist. So make sure that you terminate this loop, if necessary by adding extra RewriteCond to stop rules firing. Also delete any lower level .htaccess rewrite rulesets unless explicitly intent to use multi-level rulesets.

  2. Make sure that the syntax of each Regexp is correct by testing against a set of test patterns to make sure that is a valid syntax and does what you intend with a fully range of test URIs. See answer below for more details.

  3. Build up your rules incrementally in a test directory. You can make use of the "execute the deepest .htaccess file on the path feature" to set up a separate test directory (tree) and debug rulesets here without screwing up your main rules and stopping your site working. You have to add them one at a time because this is the only way to localise failures to individual rules.

  4. Use a dummy script stub to dump out server and environment variables. (See Listing 2)If your app uses, say, blog/index.php then you can copy this into test/blog/index.php and use it to test out your blog rules in the test subdirectory. You can also use environment variables to make sure that the rewrite engine in interpreting substitution strings correctly, e.g.

    RewriteRule ^(.*) - [E=TEST0:%{DOCUMENT_ROOT}/blog/html_cache/$1.html]
    

    and look for these REDIRECT_* variables in the phpinfo dump. BTW, I used this one and discovered on my site that I had to use %{ENV:DOCUMENT_ROOT_REAL} instead. In the case of redirector looping REDIRECT_REDIRECT_* variables list the previous pass. Etc..

  5. Make sure that you don't get bitten by your browser caching incorrect 301 redirects. See answer below. My thanks to Ulrich Palha for this.

  6. The rewrite engine seems sensitive to cascaded rules within an .htaccess context, (that is where a RewriteRule results in a substitution and this falls though to further rules), as I found bugs with internal sub-requests (1), and incorrect PATH_INFO processing which can often be prevents by use of the [NS], [L] and [PT] flags.

还有其他的评论或建议吗?

代码清单1 -- phpinfo

<?php phpinfo(INFO_ENVIRONMENT|INFO_VARIABLES);

11
这些是不错的...... 或许你应该把它们从问题中移到答案里。 - w00t
@w00t,根据您的建议,我已经将正则表达式检查器拆分出来,因为我想在其他答案中通过链接引用它。 - TerryE
3
你可能想在你的第一个建议中添加[来自文档的控制流程图](http://httpd.apache.org/docs/2.4/images/rewrite_process_uri.png)。在我看来,这比任何伪代码或解释都更容易理解,这确实是mod-rewrite voodoo最难理解的部分。 - SáT
这里有一个值得注意的提示:我花了一些时间来调试重定向和重写的问题。结果发现,当我想要重写为“/comment/”时,它实际上是重写为“/comment”。然后服务器会将其重定向到“/comment/”。对于那些习惯于Apache的人来说,这是显而易见的行为,但对于像我这样的新手来说可能不太容易理解。 - Chris
这个答案中提到的“清单2”在哪里? - pgr
显示剩余2条评论
19个回答

147

以下是关于测试规则的一些额外提示,可以帮助共享主机上的用户进行调试:

1. 使用假用户代理

在测试新规则时,添加一个条件,只使用您用于请求的fake用户代理来执行它。这样它就不会影响站点上的其他人。

例如:

#protect with a fake user agent
RewriteCond %{HTTP_USER_AGENT}  ^my-fake-user-agent$
#Here is the actual rule I am testing
RewriteCond %{HTTP_HOST} !^www\.domain\.com$ [NC] 
RewriteRule ^ http://www.domain.com%{REQUEST_URI} [L,R=302] 

如果你正在使用Firefox浏览器,可以使用User Agent Switcher来创建虚假的用户代理字符串并进行测试。

2. 测试完成前不要使用301重定向

我看到很多人在测试规则时还在使用301重定向。 不要这样做。

如果你没有在网站上使用建议1,那么不仅是你自己,任何在这个时间访问你的网站的人都将受到301的影响。

请记住,它们是永久性的,并且被你的浏览器积极缓存。 在确定无误之前,请使用302替代它,然后再更改为301。

3. 请记住,301在浏览器中被积极缓存

如果你的规则没有起作用,但看起来应该是正确的,并且你没有使用建议1和2,则在清除浏览器缓存或在私密浏览中重新测试。

4. 使用HTTP捕获工具

使用类似Fiddler的HTTP捕获工具来查看浏览器和服务器之间的实际HTTP流量。

即使其他人可能会说你的站点看起来不对,你也可以看到并报告所有图像、CSS和JS都返回404错误,从而快速缩小问题范围。

虽然其他人会报告你从URL A开始,结束于URL C,但你将能够看到它们从URL A开始,被302重定向到URL B,然后301重定向到URL C。即使URL C是最终目标,你也会知道这对SEO不利,需要修复。

你将能够看到在服务器端设置的缓存头文件,重放请求,修改请求头以进行测试......



9
Ulrich,非常感谢你的意见。你提出了一些我没有考虑到的方面。关于301调试问题,我使用Chrome的“隐身浏览”模式(也称为“色情模式”),因为关闭窗口时会删除此状态信息。希望你不介意我不将其作为一个重要的点来“接受”,因为它并没有一个唯一的最佳答案。再次感谢。 :) - TerryE
1
为了明确(你的代码中有它但没有注意到),但是为了确保你使用302而不是301重定向,你需要[L,R = 302] - icc97
8
不需要明确指定[L, R=302],只需使用[L,R]即可,默认值为302 - Rahil Wazir
4
请注意"Chrome > 设置 > 通用 > 在打开开发者工具时禁用缓存"复选框。 - johnsnails

94

在线 .htaccess 重写测试

我在谷歌搜索正则表达式帮助时找到了这个网站,它为我节省了很多时间,因为我不必每次进行小修改时都上传新的.htaccess文件。

这个网站提供如下服务:

htaccess 测试

要测试您的 htaccess 重写规则,请简单填写您正在应用规则的 url,将您的 htaccess 内容粘贴到输入区域,然后点击“立即检查”按钮。


7
谢谢您提供这个工具的指引,我发现这是解决我的问题最直接的方法。 - BobHy
如果您可以访问您的网络空间的ssh,另一个选项是通过服务器上的编辑器直接更改.htaccess。 - sjas
似乎引用的链接有问题,不能总是给出精确的输出。请务必在实际的Apache上进行检查,以确保准确性。 - Parth Soni
2
看起来很不错,但它缺少很多功能。不幸的是,这不是一个可靠的工具。 - Honsa Stunna
非常有帮助,非常感谢! - undefined
显示剩余2条评论

18

不要忘记,在 .htaccess 文件中匹配的是相对 URL。

在 .htaccess 文件中,以下 RewriteRule 永远不会匹配:

RewriteRule ^/(.*)     /something/$s

4
是的,输入 RewriteRule 的字符串是相对路径,因此会剥离任何前导的“/”符号,但是这种剥离不适用于在RewriteCond命令中组装的匹配字符串。 - TerryE

12

设置环境变量并使用头文件接收它们:

您可以通过RewriteRule行创建新的环境变量,如OP所提到的:

RewriteRule ^(.*) - [E=TEST0:%{DOCUMENT_ROOT}/blog/html_cache/$1.html]

但是如果您无法让服务器端脚本工作,那么您如何读取此环境变量呢?一种解决方案是设置头文件:

Header set TEST_FOOBAR "%{REDIRECT_TEST0}e"

该值接受格式说明符,包括环境变量的%{NAME}e说明符(不要忘记小写字母 e)。有时,您需要添加REDIRECT_前缀,但我还没有弄清楚什么时候会添加前缀,什么时候不会。


你对何时使用或不使用 REDIRECT_ 前缀有更深入的了解吗?我在其他(htaccess)上下文中也看到了关于前缀的术语,但从来没有清楚地说明是什么意思。这是否意味着当使用某些命令(但不是其他命令)时,您必须使用前缀命名变量,或将前缀添加到命名变量中?您的示例是*首个同时显示变量定义和变量用法的示例,因此从这里我倾向于认为是后者!文档帮助甚少-它们假设我们已经知道太多,并提供太少的参考/链接。 - SherylHohman

10

确保每个正则表达式的语法正确

通过针对一组测试模式进行测试,以确保它是有效的语法,并使用完整范围的测试URI来执行您的意图。

请参见下面的regexpCheck.php,这是一个简单的脚本,您可以将其添加到站点的私有/测试目录中,以帮助您执行此操作。我将其保持简短而不是美观。只需将其粘贴到regexpCheck.php文件中的测试目录中即可在您的网站上使用它。这将帮助您构建任何正则表达式并针对一系列测试用例进行测试。我在这里使用PHP PCRE引擎,但是看了Apache源代码后,发现这基本上与Apache使用的引擎相同。有许多教程和指南提供模板,可以帮助您构建正则表达式技能。

清单1 -- regexpCheck.php

<html><head><title>Regexp checker</title></head><body>
<?php 
    $a_pattern= isset($_POST['pattern']) ? $_POST['pattern'] : "";
    $a_ntests = isset($_POST['ntests']) ? $_POST['ntests'] : 1;
    $a_test   = isset($_POST['test']) ? $_POST['test'] : array();
    
    $res = array(); $maxM=-1; 
    foreach($a_test as $t ){
        $rtn = @preg_match('#'.$a_pattern.'#',$t,$m);
        if($rtn == 1){
            $maxM=max($maxM,count($m));
            $res[]=array_merge( array('matched'),  $m );
        } else {
            $res[]=array(($rtn === FALSE ? 'invalid' : 'non-matched'));
        }
    } 
?> <p>&nbsp; </p>
<form method="post" action="<?php echo $_SERVER['SCRIPT_NAME'];?>">
    <label for="pl">Regexp Pattern: </label>
    <input id="p" name="pattern" size="50" value="<?php echo htmlentities($a_pattern,ENT_QUOTES,"UTF-8");;?>" />
    <label for="n">&nbsp; &nbsp; Number of test vectors: </label>
    <input id="n" name="ntests"  size="3" value="<?php echo $a_ntests;?>"/>
    <input type="submit" name="go" value="OK"/><hr/><p>&nbsp;</p>
    <table><thead><tr><td><b>Test Vector</b></td><td>&nbsp; &nbsp; <b>Result</b></td>
<?php 
    for ( $i=0; $i<$maxM; $i++ ) echo "<td>&nbsp; &nbsp; <b>\$$i</b></td>";
    echo "</tr><tbody>\n";
    for( $i=0; $i<$a_ntests; $i++ ){
        echo '<tr><td>&nbsp;<input name="test[]" value="', 
            htmlentities($a_test[$i], ENT_QUOTES,"UTF-8"),'" /></td>';
        foreach ($res[$i] as $v) { echo '<td>&nbsp; &nbsp; ',htmlentities($v, ENT_QUOTES,"UTF-8"),'&nbsp; &nbsp; </td>';}
        echo "</tr>\n";
    }
?> </table></form></body></html>

1
快速提示:import_request_variables在PHP 5.3中已被弃用,并在5.4中删除。 extract($ _GET)extract($ _POST)结合使用可以执行相同的功能,但所有变量都需要从其名称中删除前缀。来源:http://php.net/manual/en/function.import-request-variables.php - Jeff Lambert
@watcher,谢谢。我一年前已经将我的本地版本更新为5.4兼容,但是忘记修改这篇文章了。现在已经完成了。 - TerryE
哦,天啊,即使编辑过后,仅仅复制你的代码还是得不到好的结果...但是有了正则表达式工具,我认为你的工具已经过时了。看看这些很酷的工具吧:https://regex101.com 或 http://refiddle.com 或 http://www.regexr.com - hexerei software
@hexereisoftware,这篇文章已经有3年了,所以现在使用的PHP版本和Apache版本可能会有一些细微的问题。然而,正则表达式有许多变体,每个变体都有微妙的差异。正如我所说,Apache代码使用的是一个非常类似于PHP引擎的PCRE引擎。我不确定与其他变体(如.Net)的差异是什么,因此虽然您建议使用在线资源是一个好主意,但我会坚持使用明确支持Apache或PHP语法的资源。 :-) - TerryE
Perl可能是最接近的,但PHP使用相同的语法。 - hexerei software

9

以下是需要翻译的内容:

我浪费了几个小时,只为解决一个问题:

如果您已经遵循了所有这些技巧,但仍然出现500错误,可能问题不在于 .htaccess 文件,而在于它重定向到的文件。

在我解决了我的 .htaccess 问题之后,我又花费了两个小时试图进一步修复它,尽管我只是忘记了某些权限。


我使用共享访问托管的网络服务来搭建我的个人网站,但是我所做的是设置了一个测试虚拟机,该虚拟机在PHP / Apache配置、主目录等方面与此大致相同。但是由于这台虚拟机在我的管理员权限下,因此我可以启用重写日志记录以诊断任何困难的“.htaccess”问题。 - TerryE

8
请确保在变量前使用百分号而不是美元符号。
应该使用%{HTTP_HOST},而不是${HTTP_HOST}。如果使用了错误的符号,将不会在错误日志中看到任何东西,也不会出现内部服务器错误,您的正则表达式仍然正确,只是规则无法匹配。如果您经常使用django / genshi模板并且习惯于使用${}进行变量替换,那么这真的很糟糕。

1
是的,$ 替换变量与最后一个 RewriteRule 模式相关联,% 变量与最后一个 RewriteCond 模式和特殊模式(如 %{env:XXX})相关联。 - TerryE

6

如果您正在创建重定向,请使用curl进行测试,以避免浏览器缓存问题。 使用-I仅获取http标头。 使用-L跟随所有重定向。


5
关于第4点,您仍然需要确保您的“虚拟脚本存根”在所有重写完成后实际上是目标网址,否则您将看不到任何内容!
类似/相关的技巧(请参见此问题)是插入临时规则,例如:
RewriteRule (.*) /show.php?url=$1 [END]

这里的show.php是一个非常简单的脚本,它只显示它的$_GET参数(如果你愿意,也可以显示环境变量)。

这将停止重写在您将其插入规则集的点,就像调试器中的断点一样。

如果您使用的是Apache <2.3.9,则需要使用[L]而不是[END],然后您可能需要添加:

RewriteRule ^show.php$ - [L]

在你的规则集的顶部,如果URL /show.php本身被重写。

4

在编写 .htaccess 文件时,我发现有些错误。

在多个规则中反复使用 ^(.*)$ 会导致其他规则无效,因为它可以单次匹配所有 URL。

所以如果我们针对这个网址使用规则 sapmle/url 它也将使用这个网址 sapmle/url/string


[L] 标记应该被使用来确保我们的规则已经完成处理。


需要了解:

%n 和 $n 的区别

%n%{RewriteCond} 部分进行匹配,$n%{RewriteRule} 部分进行匹配。

RewriteBase 的工作方式

RewriteBase 指令指定用于每个目录下的(htaccess)RewriteRule 指令的 URL 前缀,用于替换相对路径。

在 htaccess 上下文中使用相对路径进行替换时,除非满足以下任何一种条件,否则需要此指令:

原始请求和替换都在 DocumentRoot 下(而不是通过其他方式可访问,如 Alias)。包含 RewriteRule 的目录的文件系统路径,加上相对替换,也可以作为服务器上的 URL 路径(这很少见)。在 Apache HTTP Server 2.4.16 及更高版本中,如果请求通过 Alias 或 mod_userdir 映射,则可以省略此指令。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接