Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prefer lower case headers. #3

Merged
merged 1 commit into from
Feb 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ Rack::Cache
===========

Rack::Cache is suitable as a quick drop-in component to enable HTTP caching for
Rack-based applications that produce freshness (Expires, Cache-Control) and/or
validation (Last-Modified, ETag) information:
Rack-based applications that produce freshness (`expires`, `cache-control`)
and/or validation (`last-modified`, `etag`) information:

* Standards-based (RFC 2616)
* Freshness/expiration based caching
* Validation (If-Modified-Since / If-None-Match)
* Vary support
* Cache-Control: public, private, max-age, s-maxage, must-revalidate,
and proxy-revalidate.
* Validation (`if-modified-since` / `if-none-match`)
* `vary` support
* `cache-control` `public`, `private`, `max-age`, `s-maxage`, `must-revalidate`,
and `proxy-revalidate`.
* Portable: 100% Ruby / works with any Rack-enabled framework
* Disk, memcached, and heap memory storage backends

Expand Down Expand Up @@ -95,7 +95,7 @@ Noop entity store

Does not persist response bodies (no disk/memory used).<br/>
Responses from the cache will have an empty body.<br/>
Clients must ignore these empty cached response (check for X-Rack-Cache response header).<br/>
Clients must ignore these empty cached response (check for `x-rack-cache` response header).<br/>
Atm cannot handle streamed responses, patch needed.

```Ruby
Expand Down
12 changes: 6 additions & 6 deletions doc/configuration.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ configuration context to write messages to the errors stream.

An integer specifying the number of seconds a cached object should be considered
"fresh" when no explicit freshness information is provided in a response.
Explicit `Cache-Control` or `Expires` response headers always override this
Explicit `cache-control` or `expires` response headers always override this
value. The `default_ttl` option defaults to `0`, meaning responses without
explicit freshness information are considered immediately "stale" and will not
be served from cache without validation.
Expand Down Expand Up @@ -79,17 +79,17 @@ An array of request header names that cause the response to be treated with
private cache control semantics. The default value is `['Authorization', 'Cookie']`.
If any of these headers are present in the request, the response is considered
private and will not be cached _unless_ the response is explicitly marked public
(e.g., `Cache-Control: public`).
(e.g., `cache-control: public`).

### `allow_reload`

A boolean specifying whether reload requests sent by the client should be
honored by the cache. When this option is enabled (`rack-cache.allow_reload`
is `true`), requests that include a `Cache-Control: no-cache` header cause
is `true`), requests that include a `cache-control: no-cache` header cause
the cache to discard anything it has stored for the request and ask that the
response be fully generated.

Most browsers include a `Cache-Control: no-cache` header when the user performs
Most browsers include a `cache-control: no-cache` header when the user performs
a "hard refresh" (e.g., holding `Shift` while clicking the "Refresh" button).

*IMPORTANT: Enabling this option globally allows all clients to break your cache.*
Expand All @@ -98,11 +98,11 @@ a "hard refresh" (e.g., holding `Shift` while clicking the "Refresh" button).

A boolean specifying whether revalidate requests sent by the client should be
honored by the cache. When this option is enabled (`rack-cache.allow_revalidate`
is `true`), requests that include a `Cache-Control: max-age=0` header cause the
is `true`), requests that include a `cache-control: max-age=0` header cause the
cache to assume its copy of the response is stale, resulting in a conditional
GET / validation request to be sent to the server.

Most browsers include a `Cache-Control: max-age=0` header when the user performs
Most browsers include a `cache-control: max-age=0` header when the user performs
a refresh (e.g., clicking the "Refresh" button).

*IMPORTANT: Enabling this option globally allows all clients to break your cache.*
Expand Down
12 changes: 6 additions & 6 deletions doc/faq.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ approach to caching.

__Rack::Cache__ takes a standards-based approach to caching that provides some
benefits over framework-integrated systems. It uses standard HTTP headers
(`Expires`, `Cache-Control`, `Etag`, `Last-Modified`, etc.) to determine
(`expires`, `cache-control`, `etag`, `last-modified`, etc.) to determine
what/when to cache. Designing applications to support these standard HTTP
mechanisms gives the benefit of being able to switch to a different HTTP
cache implementation in the future.
Expand Down Expand Up @@ -74,9 +74,9 @@ Features
### Q: Does Rack::Cache support validation?

Yes. Both freshness and validation-based caching is supported. A response
will be cached if it has a freshness lifetime (e.g., `Expires` or
`Cache-Control: max-age=N` headers) and/or includes a validator (e.g.,
`Last-Modified` or `ETag` headers). When the cache hits and the response is
will be cached if it has a freshness lifetime (e.g., `expires` or
`cache-control: max-age=N` headers) and/or includes a validator (e.g.,
`last-modified` or `etag` headers). When the cache hits and the response is
fresh, it's delivered immediately without talking to the backend application;
when the cache is stale, the cached response is validated using a conditional
GET request.
Expand All @@ -103,9 +103,9 @@ own cache policy.
Although planned, there is currently no mechanism for manually purging
an entry stored in the cache.

Note that using an `Expires` or `Cache-Control: max-age=N` header and relying on
Note that using an `expires` or `cache-control: max-age=N` header and relying on
manual purge to invalidate cached entry can often be implemented more simply
using efficient validation based caching (`Last-Modified`, `Etag`). Many web
using efficient validation based caching (`last-modified`, `etag`). Many web
frameworks are based entirely on manual purge and do not support validation at
the cache level.

Expand Down
6 changes: 3 additions & 3 deletions doc/index.markdown
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
__Rack::Cache__ is suitable as a quick drop-in component to enable HTTP caching
for [Rack][]-based applications that produce freshness (`Expires`,
`Cache-Control`) and/or validation (`Last-Modified`, `ETag`) information.
for [Rack][]-based applications that produce freshness (`expires`,
`cache-control`) and/or validation (`last-modified`, `etag`) information.

* Standards-based (see [RFC 2616][rfc] / [Section 13][s13]).
* Freshness/expiration based caching
* Validation
* Vary support
* `vary` support
* Portable: 100% Ruby / works with any [Rack][]-enabled framework.
* Disk, memcached, and heap memory [storage backends][storage].

Expand Down
2 changes: 1 addition & 1 deletion doc/layout.html.erb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<!DOCTYPE html>
<html lang='en'>
<head>
<meta http-equiv='Content-Type' content='text/html;charset=utf-8'>
<meta http-equiv='content-type' content='text/html;charset=utf-8'>
<title>Rack::Cache <%= title %></title>
<link rel='stylesheet' href='rack-cache.css' type='text/css' media='all'>
<script type='text/javascript' src='http://code.jquery.com/jquery-1.2.3.js'></script>
Expand Down
4 changes: 2 additions & 2 deletions lib/rack/cache.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
# = HTTP Caching For Rack
#
# Rack::Cache is suitable as a quick, drop-in component to enable HTTP caching
# for Rack-enabled applications that produce freshness (+Expires+, +Cache-Control+)
# and/or validation (+Last-Modified+, +ETag+) information.
# for Rack-enabled applications that produce freshness (+expires+, +cache-control+)
# and/or validation (+last-modified+, +etag+) information.
#
# * Standards-based (RFC 2616 compliance)
# * Freshness/expiration based caching and validation
Expand Down
22 changes: 11 additions & 11 deletions lib/rack/cache/cache_control.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
module Rack
module Cache

# Parses a Cache-Control header and exposes the directives as a Hash.
# Parses a cache-control header and exposes the directives as a Hash.
# Directives that do not have values are set to +true+.
class CacheControl < Hash
def initialize(value=nil)
Expand Down Expand Up @@ -80,7 +80,7 @@ def no_store?
end

# The expiration time of an entity MAY be specified by the origin
# server using the Expires header (see section 14.21). Alternatively,
# server using the expires header (see section 14.21). Alternatively,
# it MAY be specified using the max-age directive in a response. When
# the max-age cache-control directive is present in a cached response,
# the response is stale if its current age is greater than the age
Expand All @@ -89,19 +89,19 @@ def no_store?
# response is cacheable (i.e., "public") unless some other, more
# restrictive cache directive is also present.
#
# If a response includes both an Expires header and a max-age
# directive, the max-age directive overrides the Expires header, even
# if the Expires header is more restrictive. This rule allows an origin
# If a response includes both an expires header and a max-age
# directive, the max-age directive overrides the expires header, even
# if the expires header is more restrictive. This rule allows an origin
# server to provide, for a given response, a longer expiration time to
# an HTTP/1.1 (or later) cache than to an HTTP/1.0 cache. This might be
# useful if certain HTTP/1.0 caches improperly calculate ages or
# expiration times, perhaps due to desynchronized clocks.
#
# Many HTTP/1.0 cache implementations will treat an Expires value that
# Many HTTP/1.0 cache implementations will treat an expires value that
# is less than or equal to the response Date value as being equivalent
# to the Cache-Control response directive "no-cache". If an HTTP/1.1
# to the cache-control response directive "no-cache". If an HTTP/1.1
# cache receives such a response, and the response does not include a
# Cache-Control header field, it SHOULD consider the response to be
# cache-control header field, it SHOULD consider the response to be
# non-cacheable in order to retain compatibility with HTTP/1.0 servers.
#
# When the max-age directive is included in the request, it indicates
Expand All @@ -114,7 +114,7 @@ def max_age
# If a response includes an s-maxage directive, then for a shared
# cache (but not for a private cache), the maximum age specified by
# this directive overrides the maximum age specified by either the
# max-age directive or the Expires header. The s-maxage directive
# max-age directive or the expires header. The s-maxage directive
# also implies the semantics of the proxy-revalidate directive. i.e.,
# that the shared cache must not use the entry after it becomes stale
# to respond to a subsequent request without first revalidating it with
Expand All @@ -128,7 +128,7 @@ def shared_max_age
# If a response includes a r-maxage directive, then for a reverse cache
# (but not for a private or proxy cache), the maximum age specified by
# this directive overrides the maximum age specified by either the max-age
# directive, the s-maxage directive, or the Expires header. The r-maxage
# directive, the s-maxage directive, or the expires header. The r-maxage
# directive also implies the semantics of the proxy-revalidate directive.
# i.e., that the reverse cache must not use the entry after it becomes
# stale to respond to a subsequent request without first revalidating it
Expand All @@ -148,7 +148,7 @@ def reverse_max_age
# MUST NOT use the entry after it becomes stale to respond to a
# subsequent request without first revalidating it with the origin
# server. (I.e., the cache MUST do an end-to-end revalidation every
# time, if, based solely on the origin server's Expires or max-age
# time, if, based solely on the origin server's expires or max-age
# value, the cached response is stale.)
#
# The must-revalidate directive is necessary to support reliable
Expand Down
18 changes: 9 additions & 9 deletions lib/rack/cache/context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,9 @@ def call!(env)
end
end

# log trace and set X-Rack-Cache tracing header
# log trace and set x-rack-cache tracing header
trace = @trace.join(', ')
response.headers['X-Rack-Cache'] = trace
response.headers['x-rack-cache'] = trace

# write log message to rack.errors
if verbose?
Expand Down Expand Up @@ -113,7 +113,7 @@ def private_request?
@private_header_keys.any? { |key| @env.key?(key) }
end

# Determine if the #response validators (ETag, Last-Modified) matches
# Determine if the #response validators (etag, last-modified) matches
# a conditional value specified in #request.
def not_modified?(response)
last_modified = @request.env['HTTP_IF_MODIFIED_SINCE']
Expand Down Expand Up @@ -181,7 +181,7 @@ def lookup
if entry
if fresh_enough?(entry)
record :fresh
entry.headers['Age'] = entry.age.to_s
entry.headers['age'] = entry.age.to_s
entry
else
record :stale
Expand All @@ -204,7 +204,7 @@ def validate_with_stale_cache_failover(entry)
rescue => e
record :connnection_failed
age = entry.age.to_s
entry.headers['Age'] = age
entry.headers['age'] = age
record "Fail-over to stale cache data with age #{age} due to #{e.class.name}: #{e}"
entry
end
Expand Down Expand Up @@ -232,12 +232,12 @@ def validate(entry)
record :valid

# Check if the response validated which is not cached here
etag = response.headers['ETag']
etag = response.headers['etag']
return response if etag && request_etags.include?(etag) && !cached_etags.include?(etag)

entry = entry.dup
entry.headers.delete('Date')
%w[Date Expires Cache-Control ETag Last-Modified].each do |name|
entry.headers.delete('date')
%w[Date expires cache-control etag last-modified].each do |name|
next unless value = response.headers[name]
entry.headers[name] = value
end
Expand Down Expand Up @@ -287,7 +287,7 @@ def fetch
def store(response)
strip_ignore_headers(response)
metastore.store(@request, response, entitystore)
response.headers['Age'] = response.age.to_s
response.headers['age'] = response.age.to_s
rescue => e
log_error(e)
nil
Expand Down
2 changes: 1 addition & 1 deletion lib/rack/cache/entity_store.rb
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,7 @@ def self.resolve(uri)
# Set `entitystore` to 'noop:/'.
# Does not persist response bodies (no disk/memory used).
# Responses from the cache will have an empty body.
# Clients must ignore these empty cached response (check for X-Rack-Cache response header).
# Clients must ignore these empty cached response (check for x-rack-cache response header).
# Atm cannot handle streamed responses, patch needed.
#
class Noop < EntityStore
Expand Down
20 changes: 10 additions & 10 deletions lib/rack/cache/meta_store.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,11 @@ def lookup(request, entity_store)

# find a cached entry that matches the request.
env = request.env
match = entries.detect{ |req,res| requests_match?((res['Vary'] || res['vary']), env, req) }
match = entries.detect{ |req,res| requests_match?((res['vary'] || res['vary']), env, req) }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was it intentional to check the same value twice? Before, it was checking if there was a match for both the Capitalized Vary and the lowercase vary. But now that everything is lowercase, we only need to check one of them, right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's correct, do you have time to submit a PR fixing this? Thanks!

return nil if match.nil?

_, res = match
if body = entity_store.open(res['X-Content-Digest'])
if body = entity_store.open(res['x-content-digest'])

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With this change, after deploying with the updated version of this gem, you can end up in a scenario where a long-term cached item still exists in the metastore with the capitalized header, but when it goes to get the key here using the lowercase header, it returns nil, and passes nil to the open method, resulting in an exception further down, and since on lookups on exceptions, it calls pass instead of fetch, it isn't going to store the response with the updated header and will continue to hit the error

Could check here for both cases, but I think another fine option is just letting people know they should flush their cache if they're upgrading from an earlier version to this one or later.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mind creating an issue for this?

restore_response(res, body)
else
# the metastore referenced an entity that doesn't exist in
Expand All @@ -64,14 +64,14 @@ def store(request, response, entity_store)

# write the response body to the entity store if this is the
# original response.
if response.headers['X-Content-Digest'].nil?
if response.headers['x-content-digest'].nil?
if request.env['rack-cache.use_native_ttl'] && response.fresh?
digest, size = entity_store.write(response.body, response.ttl)
else
digest, size = entity_store.write(response.body)
end
response.headers['X-Content-Digest'] = digest
response.headers['Content-Length'] = size.to_s unless response.headers['Transfer-Encoding']
response.headers['x-content-digest'] = digest
response.headers['content-length'] = size.to_s unless response.headers['Transfer-Encoding']

# If the entitystore backend is a Noop, do not try to read the body from the backend, it always returns an empty array
unless entity_store.is_a? Rack::Cache::EntityStore::Noop
Expand All @@ -91,12 +91,12 @@ def store(request, response, entity_store)
vary = response.vary
entries =
read(key).reject do |env, res|
(vary == (res['Vary'] || res['vary'])) &&
(vary == (res['vary'] || res['vary'])) &&
requests_match?(vary, env, stored_env)
end

headers = persist_response(response)
headers.delete('Age')
headers.delete('age')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar comment here. Is it necessary to delete the same header twice?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, if you can submit a PR to fix this, please do! Thanks!

headers.delete('age')

entries.unshift [stored_env, headers]
Expand Down Expand Up @@ -146,13 +146,13 @@ def persist_request(request)
# Converts a stored response hash into a Response object. The caller
# is responsible for loading and passing the body if needed.
def restore_response(hash, body=[])
status = hash.delete('X-Status').to_i
status = hash.delete('x-status').to_i
Rack::Cache::Response.new(status, hash, body)
end

def persist_response(response)
hash = response.headers.to_hash
hash['X-Status'] = response.status.to_s
hash = response.headers.dup
hash['x-status'] = response.status.to_s
hash
end

Expand Down
12 changes: 6 additions & 6 deletions lib/rack/cache/options.rb
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ def option_name(key)

# The number of seconds that a cache entry should be considered
# "fresh" when no explicit freshness information is provided in
# a response. Explicit Cache-Control or Expires headers
# a response. Explicit cache-control or expires headers
# override this value.
#
# Default: 0
Expand All @@ -83,24 +83,24 @@ def option_name(key)
# example, in most cases, it makes sense to prevent cookies from being
# stored in the cache.
#
# Default: ['Set-Cookie']
# Default: ['set-cookie']
option_accessor :ignore_headers

# Set of request headers that trigger "private" cache-control behavior
# on responses that don't explicitly state whether the response is
# public or private via a Cache-Control directive. Applications that use
# public or private via a cache-control directive. Applications that use
# cookies for authorization may need to add the 'Cookie' header to this
# list.
#
# Default: ['Authorization', 'Cookie']
option_accessor :private_headers

# Specifies whether a client can force cache reload by including a
# Cache-Control "no-cache" directive in the request. Disabled by default.
# cache-control "no-cache" directive in the request. Disabled by default.
option_accessor :allow_reload

# Specifies whether a client can force cache revalidate by including a
# Cache-Control "max-age=0" directive in the request. Disabled by default.
# cache-control "max-age=0" directive in the request. Disabled by default.
option_accessor :allow_revalidate

# Specifies whether the underlying entity store's native expiration should
Expand Down Expand Up @@ -148,7 +148,7 @@ def initialize_options(options={})
'rack-cache.metastore' => 'heap:/',
'rack-cache.entitystore' => 'heap:/',
'rack-cache.default_ttl' => 0,
'rack-cache.ignore_headers' => ['Set-Cookie'],
'rack-cache.ignore_headers' => ['set-cookie'],
'rack-cache.private_headers' => ['Authorization', 'Cookie'],
'rack-cache.allow_reload' => false,
'rack-cache.allow_revalidate' => false,
Expand Down
Loading