【问题标题】:mod_rewrite not sending Vary: Accept-Language when RewriteCond matchesmod_rewrite 不发送 Vary:RewriteCond 匹配时的 Accept-Language
【发布时间】:2011-04-11 12:47:39
【问题描述】:

如果没有Accept-Language 标头并且有人尝试访问?lang=en,我有一个重写规则重定向到/。它工作正常,除了返回的标题。响应中缺少Vary: Accept-Language

RewriteCond %{HTTP:Accept-Language} ^$  
RewriteCond %{QUERY_STRING}         ^lang=en  
RewriteRule ^$                      http://www.example.com/?     [R=301,L]

Apache 文档指定:

如果在条件中使用 HTTP 标头,则此标头将添加到响应的 Vary 标头中,以防请求的条件评估为真。如果请求的条件评估为 false,则不会添加它。

条件肯定是匹配和重定向的,所以我不明白为什么 Apache 不添加语言变化。如果代理要缓存 ?lang=en 并始终重定向到 / 而不管发送的 Accept-Language 标头如何,人们就会明白为什么这将是一个真正的问题。

【问题讨论】:

  • +1 不知道标题字段是(打算)自动添加到 Vary 中的。
  • 但它确实按我的预期工作。您使用的是哪个 Apache 版本以及如何对其进行测试?
  • Accept-Language 发送后是空白,还是根本没有发送?
  • 重写对我来说很好,但我没有得到 Vary: Accept-Language,只有 Vary: Accept-Encoding 来澄清。用 Fiddler 和 web-sniffer.net 测试。阿帕奇 2.2.14。我将尝试在另一个机器上重新安装干净的 Apache,尽管我确实尝试过没有重定向并且没有运气。
  • Hmm...mod_rewrite 正确设置了标头,但是在 Apache 发送响应之前它在某处被破坏了。现在,要弄清楚发生在哪里......

标签: mod-rewrite apache2 mod-headers vary


【解决方案1】:

在窥探 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->varyctx->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。相关代码部分中的版本之间似乎没有任何显着差异。

【讨论】:

    【解决方案2】:

    您可能想尝试以下方法作为解决方法:

    <LocationMatch "^.*lang\=">
        Header onsuccess merge Vary "Accept-Language"
    </LocationMatch>
    

    【讨论】:

    • 您需要使用always 条件(而不是onsuccess)才能在重定向本身上设置此标头。但是,&lt;LocationMatch&gt; 指令仅匹配 URL 路径,而不匹配查询字符串,因此无论如何这将永远不会匹配,并且永远不会设置标头。请参阅my answer 以获得有效的解决方案。
    【解决方案3】:

    要在 redirect 响应 only 上专门设置Vary: Accept-Language HTTP 响应标头(这是这里所期望的),您需要设置一个环境变量(例如.VARY_ACCEPT_LANGUAGE) 作为重定向规则的一部分,并使用它通过Header 指令有条件地设置标头。

    您还需要将always 条件(与默认的onsuccess 相对)与Header 指令一起使用,以便在3xx 响应上设置它(即非200 次回复)。

    例如:

    # Redirect requests that have an empty Accept-Language header and "lang=en" is present
    RewriteCond %{HTTP:Accept-Language} ^$  
    RewriteCond %{QUERY_STRING} ^lang=en  
    RewriteRule ^$ /? [E=VARY_ACCEPT_LANGUAGE:1,R=301,L]
    
    # Set/Merge "Vary" header on Accept-Language redirect
    Header always merge Vary "Accept-Language" env=VARY_ACCEPT_LANGUAGE
    

    但是,Vary 标头不应仅在重定向响应上设置(当Accept-Language 标头为空时),它需要在对/?lang=en 请求的所有响应中设置,无论是什么Accept-Language HTTP 请求头实际上设置为。因此,仅依靠 Apache 仅使用重定向来设置此标头是不够的(即使它确实按最初预期在响应上设置了标头)。

    为了在对/?lang=en 的所有响应(包括重定向)的所有响应中设置适当的Vary 标头,请执行以下操作:

    # Set env var if "/?lang=en" is requested
    RewriteCond %{QUERY_STRING} ^lang=en  
    RewriteRule ^$ - [E=VARY_ACCEPT_LANGUAGE:1]
    
    # Redirect requests that have an empty Accept-Language header and "lang=en" is present
    RewriteCond %{HTTP:Accept-Language} ^$  
    RewriteCond %{QUERY_STRING} ^lang=en  
    RewriteRule ^$ /? [R=301,L]
    
    # Set/Merge "Vary" header on all responses from "/?lang=en"
    Header always merge Vary "Accept-Language" env=VARY_ACCEPT_LANGUAGE
    

    但是,请注意,如果您有额外的内部重写指令导致重写引擎重新启动,则环境变量 VARY_ACCEPT_LANGUAGE 被重命名为 REDIRECT_VARY_ACCEPT_LANGUAGE 并且上面的 Header 指令将不会成功。你可能需要一个额外的指令来处理这个。例如:

    Header always merge Vary "Accept-Language" env=REDIRECT_VARY_ACCEPT_LANGUAGE
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2018-01-22
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-02-02
      • 1970-01-01
      相关资源
      最近更新 更多