在窥探 Apache 请求处理系统的肮脏弱点之后,事实证明该文档有些误导......但在我开始解释之前,据我所知,你在这方面受 Apache 的摆布一个。
客户问题
首先,如果不是客户端发送的,Vary响应头中不会添加头名。这是由于mod_rewrite 是如何构造该标头internally 的值的。
它使用apr_table_get()、请求的标头表和您提供的名称按名称查找标头:
const char *val = apr_table_get(ctx->r->headers_in, name);
如果name 不是表中的键,则此函数将返回NULL。这是一个问题,因为紧接着是对val 的检查:
if (val) {
// Set the structure member ctx->vary_this
}
ctx->vary_this 用于在每个RewriteCond 的基础上累积应组装到最终 Vary 标头*中的标头名称。因为如果没有值就不会发生赋值或追加,所以引用(但未发送)的标头将永远不会出现在Vary 中。该文档没有明确说明这一点,因此它可能是也可能不是您所期望的。
*顺便说一句,NV(无变化)标志和忽略失败功能是通过将 ctx->vary_this 设置为 NULL 来实现的,从而防止将其添加到响应标头中。
但是,您可能发送了 Accept-Language,但它是空白的。在这种情况下,空字符串将通过上述检查,并且标题名称将由 mod_rewrite 根据上述内容添加到 Vary。牢记这一点,我使用以下请求来诊断发生了什么:
用户代理:提琴手
接受:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
接受语言:
接受编码:gzip,deflate
接受字符集:ISO-8859-1,utf-8;q=0.7,*;q=0.7
保活:115
连接:保持活动
主机:129.168.0.123
这也不起作用,但为什么呢? mod_rewrite 肯定会在规则和条件匹配时设置标题(ctx->vary 是 ctx->vary_this 在所有检查条件中的聚合):
if (ctx->vary) {
apr_table_merge(r->headers_out, "Vary", ctx->vary);
}
这可以通过日志语句验证,r->headers_out是生成响应头时使用的变量。既然肯定有问题,规则执行后肯定有问题。
.htaccess 问题
目前,您似乎正在.htaccess 或<Directory> 部分中定义您的规则。这意味着mod_rewrite 运行在 Apache 的修复阶段,它在这里实际执行重写的机制非常混乱。让我们假设没有外部重定向,因为即使没有它,您也会遇到问题(稍后我会解决重定向的问题)。
执行重写后,模块在请求处理中实际映射到文件为时已晚。相反,它会将自己指定为请求的“内容”处理程序,当请求到达该点时,它会调用ap_internal_redirect()。这会导致创建一个新的请求对象,它不包含原始的 headers_out 表。
假设mod_rewrite 不会导致进一步的重定向,则响应是从 new 请求对象生成的,该对象永远不会分配适当的(原始)标头。可以通过在每个服务器上下文中工作(在主配置中或在 <VirtualHost> 中)来解决这个问题,但是...
重定向问题
不幸的是,事实证明这在很大程度上是无关紧要的,因为即使我们在服务器上下文中使用mod_rewrite,在发生重定向时响应所采用的路径仍然会导致模块设置的标头被丢弃出去。
当 Apache 接收到请求时,它会通过一系列函数调用到达ap_process_request()。这又会调用ap_process_request_internal(),其中大部分重要的请求解析步骤都会发生(包括mod_rewrite 的调用)。它返回一个整数状态码,在重定向的情况下恰好设置为 301。
大多数请求返回 OK(其值为 0),立即导致 ap_finalize_request_protocol()。但是,那是not the case here:
if (access_status == OK) {
ap_finalize_request_protocol(r);
}
else {
r->status = HTTP_OK;
ap_die(access_status, r);
}
ap_die() 做了一些额外的操作(比如将响应代码返回到 301),并且在这种特殊情况下以调用 ap_send_error_response() 结束。
幸运的是,这终于是问题的根源。尽管看起来像这样,但事情并不是“倒退”,这会导致原始标头的破坏。甚至还有评论in the source:
if (!r->assbackwards) {
apr_table_t *tmp = r->headers_out;
/* For all HTTP/1.x responses for which we generate the message,
* we need to avoid inheriting the "normal status" header fields
* that may have been set by the request handler before the
* error or redirect, except for Location on external redirects.
*/
r->headers_out = r->err_headers_out;
r->err_headers_out = tmp;
apr_table_clear(r->err_headers_out);
if (ap_is_HTTP_REDIRECT(status) || (status == HTTP_CREATED)) {
if ((location != NULL) && *location) {
apr_table_setn(r->headers_out, "Location", location);
}
//...
}
//...
}
注意r->headers_out被替换了,原来的表被清空了。该表包含预期会在响应中显示的所有信息,所以现在它丢失了。
结论
如果您不重定向并且在每个服务器上下文中定义规则,那么一切似乎都可以正常工作。然而,这不是你想要的。我可以看到一个潜在的解决方法,但我不确定它是否可以接受,更不用说需要重新编译服务器了。
至于Vary: Accept-Encoding,我只能假设它来自一个不同的模块,该模块的行为方式允许标题潜入。我也不确定为什么Gumbo 在尝试时没有问题。
作为参考,我正在查看 2.2.14 和 2.2 trunk source code,我正在修改和运行 Apache 2.2.15。相关代码部分中的版本之间似乎没有任何显着差异。