Rails 使用条件请求,服务器日志显示返回 200,但客户端接收为 304

spike76 · 2020年04月03日 · 最后由 spike76 回复于 2020年04月09日 · 4168 次阅读

一段很简单的代码 (rails 6)

class HomeController < ApplicationController
  def index
    @subjects = Subject.all
    if stale? @subjects
      render plain: 'stale', status: 200
    end
  end
end

执行curl -i 'http://127.0.0.1:3000/' -H 'If-None-Match: W/"bfd6f8bb9d7c5c210ab13b06a5cfcde1"' -H 'If-Modified-Since: Thu, 02 Apr 2020 08:01:57 GMT' --compressed

由于 if-modified-since 时间早于@subjects中最大的 updated_at, 所以会执行render plain: 'stale', status: 200,IDE 中看到日志也是Completed 200 OK in 15ms ,断点执行 response.status 返回也是 200,但 curl 中的响应结果却是 304,如下

HTTP/1.1 304 Not Modified
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
X-Download-Options: noopen
X-Permitted-Cross-Domain-Policies: none
Referrer-Policy: strict-origin-when-cross-origin
ETag: W/"bfd6f8bb9d7c5c210ab13b06a5cfcde1"
Last-Modified: Fri, 03 Apr 2020 02:06:29 GMT
Cache-Control: max-age=0, private, must-revalidate
X-Request-Id: a42b9511-1df3-490b-8230-f6400fa92d1b
X-Runtime: 0.515872

使用 charlse 抓包确实是 304。

但如果把render plain: 'stale', status: 200中的 status 改为 201 或其他值,同样的 curl 请求返回的结果就是设置的值。

想不通。求助大佬们

你可以搜索下 http 304 的含义是啥,好像是跟缓存有关

http 304 客户端有缓冲的文档并发出了一个条件性的请求。服务器告诉客户端,原来缓冲的文档还可以继续使用

liuminhan 回复

我知道 304 是使用客户端缓存,关键是我的服务器的 response status 是 200,为什么到了客户端就成了 304

liuminhan 回复

兄弟你还是没明白我的问题。论坛里相关的帖子我都看过了

当请求的资源没更新时,在 stale? @subjects时就会返回 304,客户端收到的也是 304,这是正常的,使用了客户端缓存。

但资源更新后,用同样的请求,会执行render plain: 'stale', status: 200,从日志看,服务器返回的是 200。到这一步为止也是正常的。问题时客户端收到的不是 200,而是 304。

而将 render 中的 status 改成 201、202、203 之类的,同样的请求,客户端就能正常显示 201、202、203,而非 304

5 楼 已删除

我有两个怀疑的点

Rack

  1. Rack::Etag 会自动帮 http code 为 200 的 response 计算 etag

  2. Rack::ConditionalGet 会比较 etag 和 last_modifed. 如果 etag 和请求的 If-None-Match 匹配的话,它会自动帮你改为 304

对应关系

from RFC 7232:

A recipient must ignore If-Modified-Since if the request contains an If-None-Match header field; the condition in If-None-Match is considered to be a more accurate replacement for the condition in If-Modified-Since, and the two are only combined for the sake of interoperating with older intermediaries that might not implement If-None-Match.

etag 的优先级高于 last_modified, 当你的 response 含有 etag 时,浏览器可能会忽略掉 last_modified。

Ngninx

nginx 也会帮你重新计算了 etag,把你的 nginx 的这个选项关掉试试?

Syntax:     etag on | off;
Default:    etag on;

Context:    http, server, location

This directive appeared in version 1.3.3.

参考资料

https://stackoverflow.com/a/24555092/5117552

https://stackoverflow.com/questions/25322053/what-if-both-if-modified-since-and-if-none-match-are-present-in-http-headers

你可以试试这个命令,把 etag 改成乱七八糟不存在的值,看看返回的是不是 200

curl -i 'http://127.0.0.1:3000/' -H 'If-None-Match: not-exist-etag' -H 'If-Modified-Since: Thu, 02 Apr 2020 08:01:57 GMT' --compressed

8 楼 已删除
xiaoronglv 回复

谢谢大佬的认真回复。nginx 一直是关闭的。

etag 的变化其实不在这个问题讨论的范畴了。etag 一旦不一致,服务端确实会返回 200,客户端也会收到 200。

Rack::ConditionalGet 会比较 etag 和 last_modifed. 如果 etag 和请求的 If-None-Match 匹配的话,它会自动帮你改为 304

之前看过这一段源码

def fresh_when(object = nil, etag: nil, weak_etag: nil, strong_etag: nil, last_modified: nil, public: false, template: nil)
  weak_etag ||= etag || object unless strong_etag
  last_modified ||= object.try(:updated_at) || object.try(:maximum, :updated_at)

  if strong_etag
    response.strong_etag = combine_etags strong_etag,
      last_modified: last_modified, public: public, template: template
  elsif weak_etag || template
    response.weak_etag = combine_etags weak_etag,
      last_modified: last_modified, public: public, template: template
  end

  response.last_modified = last_modified if last_modified
  response.cache_control[:public] = true if public

  head :not_modified if request.fresh?(response)
end

    def fresh?(response)
      last_modified = if_modified_since
      etag          = if_none_match

      return false unless last_modified || etag

      success = true
      success &&= not_modified?(response.last_modified) if last_modified
      success &&= etag_matches?(response.etag) if etag
      success
    end

fresh_when 的最后一行的逻辑就是返回 304 的响应,但需要同时满足 etag 和 If-Modified-Since 这两项条件才行。当我的 if-modified-since 使用的是较老的时间时,当然不会 head :not_modified。日志中看到 render 200,但客户端显示了 304。

我之前怀疑客户端收到的是 200,但由于某种逻辑 (比如返回的 etag 没有改变) 显示为 304,于是用 charles 抓包,但抓包结果就显示 304。

我刚才又用 rails6.0.2.2 新建了一个纯净的项目,依然遇到了这个问题。但在一个不纯净的 rails6.0.2 版本上就没有遇到此问题

原因找到了,除了 rails 的 fresh_when 有处理条件请求的逻辑外,rack 的 condition_get.rb 也有相关逻辑

Rails::Rack::Logger 先行打印日志,此时 status 为 200。

在 rack 2.2 的版本,rack/lib/rack/conditional_get.rb 中的 fresh?方法逻辑是优先判断 if-none-match,只要 if-none-match 和 etag 匹配,就不再处理 If-Modified-Since,直接返回 true。当 fresh?为 true 时 call 方法会将 200 的 status 重写为 304。因此服务器日志中看到是 200,实际客户端收到是 304

而在 rack 2.1 的版本,conditional_get.rb 中的 fresh?会判断 if-none-match 和 if-modified-since,都满足条件才会返回 true。因此在较老版本的 rails(rack 低于 2.2) 中不会遇到帖子中描述的问题。

#rack/conditional_get.rb
    def call(env)
      case env[REQUEST_METHOD]
      when "GET", "HEAD"
        status, headers, body = @app.call(env)
        headers = Utils::HeaderHash.new(headers)
        if status == 200 && fresh?(env, headers)
          status = 304
          headers.delete(CONTENT_TYPE)
          headers.delete(CONTENT_LENGTH)
          original_body = body
          body = Rack::BodyProxy.new([]) do
            original_body.close if original_body.respond_to?(:close)
          end
        end
        [status, headers, body]
      else
        @app.call(env)
      end
    end

#rack 2.1
    def fresh?(env, headers)
      modified_since = env['HTTP_IF_MODIFIED_SINCE']
      none_match     = env['HTTP_IF_NONE_MATCH']

      return false unless modified_since || none_match

      success = true
      success &&= modified_since?(to_rfc2822(modified_since), headers) if modified_since
      success &&= etag_matches?(none_match, headers) if none_match
      success
    end

#rack 2.2
    def fresh?(env, headers)
      # If-None-Match has priority over If-Modified-Since per RFC 7232
      if none_match = env['HTTP_IF_NONE_MATCH']
        etag_matches?(none_match, headers)
      elsif (modified_since = env['HTTP_IF_MODIFIED_SINCE']) && (modified_since = to_rfc2822(modified_since))
        modified_since?(modified_since, headers)
      end
    end
需要 登录 后方可回复, 如果你还没有账号请 注册新账号