rack/rack — pull request #577 by mlangenberg (2013-06-18 19:07:22+00:00)
Title: Using Rack::ETag and Rack::Deflater together does not follow spec

ETags have Weak and Strong Validators. A weak ETag validator looks like this `ETag: W/"123456789"`.

According to [spec](http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.3.3), if two response have the same ETag, their bodies should be byte-for-byte identical.

Since `md5(gzip(body))` != `md5(body)`, a request with the `Accept-Encoding: gzip` header should have a different ETag. And that is for example why Nginx clears the ETag header from responses when compressing on the fly. Makes sense, right?

Back to Rack. In order to comply, we need to do the HTTP compression first, and generate the ETag over the compressed data.
```ruby
use Rack::ETag
use Rack::Deflater
run proc { |env| [200, {"Content-Type" => "text/plain"}, ["OK"] }
```
The response will include a Strong ETag validator. But, since the [Gzip format](http://www.gzip.org/zlib/rfc-gzip.html#file-format) has a mandatory MTIME, which is [set to `Time.now` in Rack::Deflator](https://github.com/rack/rack/blob/master/lib/rack/deflater.rb#L52), the ETag of *every request* will be different.

When using the Rack middleware the other way around:
```ruby
use Rack::Deflater
use Rack::ETag
run proc { |env| [200, {"Content-Type" => "text/plain"}, ["OK"] }
```
We fail to comply to spec because a request with `Accept-Encoding: gzip` and one without will both have the same *strong* ETag validator in the response header.

## Solution 1
Calculate the _strong_ ETag from the compressed response body, but set the MTIME to 0 (Unix Epoch). It is a good thing Rack::ETag tries to use the Last-Modified header, but often it is not set for dynamic requests. The Gzip format allows this.

> MTIME (Modification TIME)
> This gives the most recent modification time of the original file being compressed. The > time is in Unix format, i.e., seconds since 00:00:00 GMT, Jan. 1, 1970. (Note that this > may cause problems for MS-DOS and other systems that use local rather than
> Universal time.) If the compressed data did not come from a file, MTIME is set to the
> time at which compression started. MTIME = 0 means no time stamp is available.

This requires a small change to Rack::Deflater.

## Solution 2
In other situations Rack::Etag is called *before* the response body is compressed. This is in the case of Rack::Deflater mounted before Rack::Etag or when Nginx is setup to do HTTP compression on-the-fly.

If we suspect that the body (or headers) are going to change after they ran through Rack::Etag, we should not add a Strong ETag validator to the response. In that case it is better to use a Weak ETag validator.

For instance if `Accept-Encoding` is set, but `Content-Encoding` is not *yet*. We could simply do `headers['ETag'] = %(W/"#{digest}")`.

Rack::Deflater will do its thing a moment later, and change the response body, but the ETag is still a perfectly valid *Weak* ETag Validator.

Any thoughts?