http 0.7.4 → 0.8.0.pre
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rspec +0 -1
- data/.rubocop.yml +5 -2
- data/CHANGES.md +24 -7
- data/CONTRIBUTING.md +25 -0
- data/Gemfile +24 -22
- data/Guardfile +2 -2
- data/README.md +34 -4
- data/Rakefile +7 -7
- data/examples/parallel_requests_with_celluloid.rb +2 -2
- data/http.gemspec +12 -12
- data/lib/http.rb +11 -10
- data/lib/http/cache.rb +146 -0
- data/lib/http/cache/headers.rb +100 -0
- data/lib/http/cache/null_cache.rb +13 -0
- data/lib/http/chainable.rb +14 -3
- data/lib/http/client.rb +64 -80
- data/lib/http/connection.rb +139 -0
- data/lib/http/content_type.rb +2 -2
- data/lib/http/errors.rb +7 -1
- data/lib/http/headers.rb +21 -8
- data/lib/http/headers/mixin.rb +1 -1
- data/lib/http/mime_type.rb +2 -2
- data/lib/http/mime_type/adapter.rb +2 -2
- data/lib/http/mime_type/json.rb +4 -4
- data/lib/http/options.rb +65 -74
- data/lib/http/redirector.rb +3 -3
- data/lib/http/request.rb +20 -13
- data/lib/http/request/caching.rb +95 -0
- data/lib/http/request/writer.rb +5 -5
- data/lib/http/response.rb +15 -9
- data/lib/http/response/body.rb +21 -8
- data/lib/http/response/caching.rb +142 -0
- data/lib/http/response/io_body.rb +63 -0
- data/lib/http/response/parser.rb +1 -1
- data/lib/http/response/status.rb +4 -12
- data/lib/http/response/status/reasons.rb +53 -53
- data/lib/http/response/string_body.rb +53 -0
- data/lib/http/version.rb +1 -1
- data/spec/lib/http/cache/headers_spec.rb +77 -0
- data/spec/lib/http/cache_spec.rb +182 -0
- data/spec/lib/http/client_spec.rb +123 -95
- data/spec/lib/http/content_type_spec.rb +25 -25
- data/spec/lib/http/headers/mixin_spec.rb +8 -8
- data/spec/lib/http/headers_spec.rb +213 -173
- data/spec/lib/http/options/body_spec.rb +5 -5
- data/spec/lib/http/options/form_spec.rb +3 -3
- data/spec/lib/http/options/headers_spec.rb +7 -7
- data/spec/lib/http/options/json_spec.rb +3 -3
- data/spec/lib/http/options/merge_spec.rb +26 -22
- data/spec/lib/http/options/new_spec.rb +10 -10
- data/spec/lib/http/options/proxy_spec.rb +8 -8
- data/spec/lib/http/options_spec.rb +2 -2
- data/spec/lib/http/redirector_spec.rb +32 -32
- data/spec/lib/http/request/caching_spec.rb +133 -0
- data/spec/lib/http/request/writer_spec.rb +26 -26
- data/spec/lib/http/request_spec.rb +63 -58
- data/spec/lib/http/response/body_spec.rb +13 -13
- data/spec/lib/http/response/caching_spec.rb +201 -0
- data/spec/lib/http/response/io_body_spec.rb +35 -0
- data/spec/lib/http/response/status_spec.rb +25 -25
- data/spec/lib/http/response/string_body_spec.rb +35 -0
- data/spec/lib/http/response_spec.rb +64 -45
- data/spec/lib/http_spec.rb +103 -76
- data/spec/spec_helper.rb +10 -12
- data/spec/support/connection_reuse_shared.rb +100 -0
- data/spec/support/create_certs.rb +12 -12
- data/spec/support/dummy_server.rb +11 -11
- data/spec/support/dummy_server/servlet.rb +43 -31
- data/spec/support/proxy_server.rb +31 -25
- metadata +57 -8
- data/spec/support/example_server.rb +0 -30
- data/spec/support/example_server/servlet.rb +0 -102
data/lib/http/content_type.rb
CHANGED
@@ -9,7 +9,7 @@ module HTTP
|
|
9
9
|
new mime_type(str), charset(str)
|
10
10
|
end
|
11
11
|
|
12
|
-
|
12
|
+
private
|
13
13
|
|
14
14
|
# :nodoc:
|
15
15
|
def mime_type(str)
|
@@ -20,7 +20,7 @@ module HTTP
|
|
20
20
|
# :nodoc:
|
21
21
|
def charset(str)
|
22
22
|
md = str.to_s.match CHARSET_RE
|
23
|
-
md && md[1].to_s.strip.gsub(/^"|"$/,
|
23
|
+
md && md[1].to_s.strip.gsub(/^"|"$/, "")
|
24
24
|
end
|
25
25
|
end
|
26
26
|
end
|
data/lib/http/errors.rb
CHANGED
@@ -8,6 +8,12 @@ module HTTP
|
|
8
8
|
# Generic Response error
|
9
9
|
class ResponseError < Error; end
|
10
10
|
|
11
|
-
#
|
11
|
+
# Requested to do something when we're in the wrong state
|
12
12
|
class StateError < ResponseError; end
|
13
|
+
|
14
|
+
# Generic Cache error
|
15
|
+
class CacheError < Error; end
|
16
|
+
|
17
|
+
# Header name is invalid
|
18
|
+
class InvalidHeaderNameError < Error; end
|
13
19
|
end
|
data/lib/http/headers.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
|
-
require
|
1
|
+
require "forwardable"
|
2
2
|
|
3
|
-
require
|
3
|
+
require "http/errors"
|
4
|
+
require "http/headers/mixin"
|
4
5
|
|
5
6
|
module HTTP
|
6
7
|
# HTTP Headers container.
|
@@ -11,6 +12,10 @@ module HTTP
|
|
11
12
|
# Matches HTTP header names when in "Canonical-Http-Format"
|
12
13
|
CANONICAL_HEADER = /^[A-Z][a-z]*(-[A-Z][a-z]*)*$/
|
13
14
|
|
15
|
+
# Matches valid header field name according to RFC.
|
16
|
+
# @see http://tools.ietf.org/html/rfc7230#section-3.2
|
17
|
+
HEADER_NAME_RE = /^[A-Za-z0-9!#\$%&'*+\-.^_`|~]+$/
|
18
|
+
|
14
19
|
# Class constructor.
|
15
20
|
def initialize
|
16
21
|
@pile = []
|
@@ -31,7 +36,7 @@ module HTTP
|
|
31
36
|
# @param [#to_s] name header name
|
32
37
|
# @return [void]
|
33
38
|
def delete(name)
|
34
|
-
name =
|
39
|
+
name = normalize_header name.to_s
|
35
40
|
@pile.delete_if { |k, _| k == name }
|
36
41
|
end
|
37
42
|
|
@@ -41,7 +46,7 @@ module HTTP
|
|
41
46
|
# @param [Array<#to_s>, #to_s] value header value(s) to be appended
|
42
47
|
# @return [void]
|
43
48
|
def add(name, value)
|
44
|
-
name =
|
49
|
+
name = normalize_header name.to_s
|
45
50
|
Array(value).each { |v| @pile << [name, v.to_s] }
|
46
51
|
end
|
47
52
|
|
@@ -52,7 +57,7 @@ module HTTP
|
|
52
57
|
#
|
53
58
|
# @return [Array<String>]
|
54
59
|
def get(name)
|
55
|
-
name =
|
60
|
+
name = normalize_header name.to_s
|
56
61
|
@pile.select { |k, _| k == name }.map { |_, v| v }
|
57
62
|
end
|
58
63
|
|
@@ -77,6 +82,7 @@ module HTTP
|
|
77
82
|
def to_h
|
78
83
|
Hash[keys.map { |k| [k, self[k]] }]
|
79
84
|
end
|
85
|
+
alias_method :to_hash, :to_h
|
80
86
|
|
81
87
|
# Returns headers key/value pairs.
|
82
88
|
#
|
@@ -179,14 +185,21 @@ module HTTP
|
|
179
185
|
alias_method :[], :coerce
|
180
186
|
end
|
181
187
|
|
182
|
-
|
188
|
+
private
|
183
189
|
|
184
190
|
# Transforms `name` to canonical HTTP header capitalization
|
185
191
|
#
|
186
192
|
# @param [String] name
|
193
|
+
# @raise [InvalidHeaderNameError] if normalized name does not
|
194
|
+
# match {HEADER_NAME_RE}
|
187
195
|
# @return [String] canonical HTTP header name
|
188
|
-
def
|
189
|
-
name[CANONICAL_HEADER]
|
196
|
+
def normalize_header(name)
|
197
|
+
normalized = name[CANONICAL_HEADER]
|
198
|
+
normalized ||= name.split(/[\-_]/).map(&:capitalize).join("-")
|
199
|
+
|
200
|
+
return normalized if normalized =~ HEADER_NAME_RE
|
201
|
+
|
202
|
+
fail InvalidHeaderNameError, "Invalid HTTP header field name: #{name.inspect}"
|
190
203
|
end
|
191
204
|
end
|
192
205
|
end
|
data/lib/http/headers/mixin.rb
CHANGED
data/lib/http/mime_type.rb
CHANGED
data/lib/http/mime_type/json.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
|
-
require
|
2
|
-
require
|
1
|
+
require "json"
|
2
|
+
require "http/mime_type/adapter"
|
3
3
|
|
4
4
|
module HTTP
|
5
5
|
module MimeType
|
@@ -17,7 +17,7 @@ module HTTP
|
|
17
17
|
end
|
18
18
|
end
|
19
19
|
|
20
|
-
register_adapter
|
21
|
-
register_alias
|
20
|
+
register_adapter "application/json", JSON
|
21
|
+
register_alias "application/json", :json
|
22
22
|
end
|
23
23
|
end
|
data/lib/http/options.rb
CHANGED
@@ -1,81 +1,78 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
require
|
1
|
+
require "http/headers"
|
2
|
+
require "openssl"
|
3
|
+
require "socket"
|
4
|
+
require "http/cache/null_cache"
|
4
5
|
|
5
6
|
module HTTP
|
6
7
|
class Options
|
7
|
-
# How to format the response [:object, :body, :parse_body]
|
8
|
-
attr_accessor :response
|
9
|
-
|
10
|
-
# HTTP headers to include in the request
|
11
|
-
attr_accessor :headers
|
12
|
-
|
13
|
-
# Query string params to add to the url
|
14
|
-
attr_accessor :params
|
15
|
-
|
16
|
-
# Form data to embed in the request
|
17
|
-
attr_accessor :form
|
18
|
-
|
19
|
-
# JSON data to embed in the request
|
20
|
-
attr_accessor :json
|
21
|
-
|
22
|
-
# Explicit request body of the request
|
23
|
-
attr_accessor :body
|
24
|
-
|
25
|
-
# HTTP proxy to route request
|
26
|
-
attr_accessor :proxy
|
27
|
-
|
28
|
-
# Socket classes
|
29
|
-
attr_accessor :socket_class, :ssl_socket_class
|
30
|
-
|
31
|
-
# SSL context
|
32
|
-
attr_accessor :ssl_context
|
33
|
-
|
34
|
-
# Follow redirects
|
35
|
-
attr_accessor :follow
|
36
|
-
|
37
|
-
protected :response=, :headers=, :proxy=, :params=, :form=, :json=, :follow=
|
38
|
-
|
39
8
|
@default_socket_class = TCPSocket
|
40
9
|
@default_ssl_socket_class = OpenSSL::SSL::SSLSocket
|
41
10
|
|
11
|
+
@default_cache = Http::Cache::NullCache.new
|
12
|
+
|
42
13
|
class << self
|
43
14
|
attr_accessor :default_socket_class, :default_ssl_socket_class
|
15
|
+
attr_accessor :default_cache
|
44
16
|
|
45
17
|
def new(options = {})
|
46
18
|
return options if options.is_a?(self)
|
47
19
|
super
|
48
20
|
end
|
21
|
+
|
22
|
+
def defined_options
|
23
|
+
@defined_options ||= []
|
24
|
+
end
|
25
|
+
|
26
|
+
protected
|
27
|
+
|
28
|
+
def def_option(name, &interpreter)
|
29
|
+
defined_options << name.to_sym
|
30
|
+
interpreter ||= ->(v) { v }
|
31
|
+
|
32
|
+
attr_accessor name
|
33
|
+
protected :"#{name}="
|
34
|
+
|
35
|
+
define_method(:"with_#{name}") do |value|
|
36
|
+
dup { |opts| opts.send(:"#{name}=", instance_exec(value, &interpreter)) }
|
37
|
+
end
|
38
|
+
end
|
49
39
|
end
|
50
40
|
|
51
41
|
def initialize(options = {})
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
42
|
+
defaults = {:response => :auto,
|
43
|
+
:proxy => {},
|
44
|
+
:socket_class => self.class.default_socket_class,
|
45
|
+
:ssl_socket_class => self.class.default_ssl_socket_class,
|
46
|
+
:cache => self.class.default_cache,
|
47
|
+
:headers => {}}
|
48
|
+
|
49
|
+
opts_w_defaults = defaults.merge(options)
|
50
|
+
opts_w_defaults[:headers] = HTTP::Headers.coerce(opts_w_defaults[:headers])
|
51
|
+
|
52
|
+
opts_w_defaults.each do |(opt_name, opt_val)|
|
53
|
+
self[opt_name] = opt_val
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def_option :headers do |headers|
|
58
|
+
self.headers.merge(headers)
|
59
|
+
end
|
60
|
+
|
61
|
+
%w(proxy params form json body follow response socket_class ssl_socket_class ssl_context persistent).each do |method_name|
|
62
|
+
def_option method_name
|
65
63
|
end
|
66
64
|
|
67
|
-
|
68
|
-
|
69
|
-
|
65
|
+
def_option :cache do |cache_or_cache_options|
|
66
|
+
if cache_or_cache_options.respond_to? :perform
|
67
|
+
cache_or_cache_options
|
68
|
+
else
|
69
|
+
require "http/cache"
|
70
|
+
HTTP::Cache.new(cache_or_cache_options)
|
70
71
|
end
|
71
72
|
end
|
72
73
|
|
73
|
-
|
74
|
-
|
75
|
-
def with_#{method_name}(value)
|
76
|
-
dup { |opts| opts.#{method_name} = value }
|
77
|
-
end
|
78
|
-
RUBY
|
74
|
+
def persistent?
|
75
|
+
!persistent.nil? && persistent != ""
|
79
76
|
end
|
80
77
|
|
81
78
|
def [](option)
|
@@ -97,22 +94,10 @@ module HTTP
|
|
97
94
|
end
|
98
95
|
|
99
96
|
def to_hash
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
:response => response,
|
105
|
-
:headers => headers.to_h,
|
106
|
-
:proxy => proxy,
|
107
|
-
:params => params,
|
108
|
-
:form => form,
|
109
|
-
:json => json,
|
110
|
-
:body => body,
|
111
|
-
:follow => follow,
|
112
|
-
:socket_class => socket_class,
|
113
|
-
:ssl_socket_class => ssl_socket_class,
|
114
|
-
:ssl_context => ssl_context
|
115
|
-
}
|
97
|
+
hash_pairs = self.class
|
98
|
+
.defined_options
|
99
|
+
.flat_map { |opt_name| [opt_name, self[opt_name]] }
|
100
|
+
Hash[*hash_pairs]
|
116
101
|
end
|
117
102
|
|
118
103
|
def dup
|
@@ -121,7 +106,13 @@ module HTTP
|
|
121
106
|
dupped
|
122
107
|
end
|
123
108
|
|
124
|
-
|
109
|
+
protected
|
110
|
+
|
111
|
+
def []=(option, val)
|
112
|
+
send(:"#{option}=", val)
|
113
|
+
end
|
114
|
+
|
115
|
+
private
|
125
116
|
|
126
117
|
def argument_error!(message)
|
127
118
|
fail(Error, message, caller[1..-1])
|
data/lib/http/redirector.rb
CHANGED
@@ -22,7 +22,7 @@ module HTTP
|
|
22
22
|
follow(&block)
|
23
23
|
end
|
24
24
|
|
25
|
-
|
25
|
+
private
|
26
26
|
|
27
27
|
# Reset redirector state
|
28
28
|
def reset(request, response)
|
@@ -38,8 +38,8 @@ module HTTP
|
|
38
38
|
fail TooManyRedirectsError if too_many_hops?
|
39
39
|
fail EndlessRedirectError if endless_loop?
|
40
40
|
|
41
|
-
uri = @response.headers[
|
42
|
-
fail StateError,
|
41
|
+
uri = @response.headers["Location"]
|
42
|
+
fail StateError, "no Location header in redirect" unless uri
|
43
43
|
|
44
44
|
if 303 == @response.code
|
45
45
|
@request = @request.redirect uri, :get
|
data/lib/http/request.rb
CHANGED
@@ -1,9 +1,11 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require
|
1
|
+
require "http/errors"
|
2
|
+
require "http/headers"
|
3
|
+
require "http/request/caching"
|
4
|
+
require "http/request/writer"
|
5
|
+
require "http/version"
|
6
|
+
require "base64"
|
7
|
+
require "uri"
|
8
|
+
require "time"
|
7
9
|
|
8
10
|
module HTTP
|
9
11
|
class Request
|
@@ -66,7 +68,7 @@ module HTTP
|
|
66
68
|
attr_reader :proxy, :body, :version
|
67
69
|
|
68
70
|
# :nodoc:
|
69
|
-
def initialize(verb, uri, headers = {}, proxy = {}, body = nil, version =
|
71
|
+
def initialize(verb, uri, headers = {}, proxy = {}, body = nil, version = "1.1") # rubocop:disable ParameterLists
|
70
72
|
@verb = verb.to_s.downcase.to_sym
|
71
73
|
@uri = uri.is_a?(URI) ? uri : URI(uri.to_s)
|
72
74
|
@scheme = @uri.scheme && @uri.scheme.to_s.downcase.to_sym
|
@@ -78,15 +80,15 @@ module HTTP
|
|
78
80
|
|
79
81
|
@headers = HTTP::Headers.coerce(headers || {})
|
80
82
|
|
81
|
-
@headers[
|
82
|
-
@headers[
|
83
|
+
@headers["Host"] ||= default_host
|
84
|
+
@headers["User-Agent"] ||= USER_AGENT
|
83
85
|
end
|
84
86
|
|
85
87
|
# Returns new Request with updated uri
|
86
88
|
def redirect(uri, verb = @verb)
|
87
89
|
uri = @uri.merge uri.to_s
|
88
90
|
req = self.class.new(verb, uri, headers, proxy, body, version)
|
89
|
-
req[
|
91
|
+
req["Host"] = req.uri.host
|
90
92
|
req
|
91
93
|
end
|
92
94
|
|
@@ -109,7 +111,7 @@ module HTTP
|
|
109
111
|
# Compute and add the Proxy-Authorization header
|
110
112
|
def include_proxy_authorization_header
|
111
113
|
digest = Base64.encode64("#{proxy[:proxy_username]}:#{proxy[:proxy_password]}").chomp
|
112
|
-
headers[
|
114
|
+
headers["Proxy-Authorization"] = "Basic #{digest}"
|
113
115
|
end
|
114
116
|
|
115
117
|
# Compute HTTP request header for direct or proxy request
|
@@ -127,7 +129,12 @@ module HTTP
|
|
127
129
|
using_proxy? ? proxy[:proxy_port] : uri.port
|
128
130
|
end
|
129
131
|
|
130
|
-
|
132
|
+
# @return [HTTP::Request::Caching]
|
133
|
+
def caching
|
134
|
+
Caching.new self
|
135
|
+
end
|
136
|
+
|
137
|
+
private
|
131
138
|
|
132
139
|
def path_for_request_header
|
133
140
|
if using_proxy?
|
@@ -139,7 +146,7 @@ module HTTP
|
|
139
146
|
|
140
147
|
def uri_path_with_query
|
141
148
|
path = uri_has_query? ? "#{uri.path}?#{uri.query}" : uri.path
|
142
|
-
path.empty? ?
|
149
|
+
path.empty? ? "/" : path
|
143
150
|
end
|
144
151
|
|
145
152
|
def uri_has_query?
|
@@ -0,0 +1,95 @@
|
|
1
|
+
require "http/cache/headers"
|
2
|
+
|
3
|
+
module HTTP
|
4
|
+
class Request
|
5
|
+
# Decorator for requests to provide convenience methods related to caching.
|
6
|
+
class Caching < DelegateClass(HTTP::Request)
|
7
|
+
INVALIDATING_METHODS = [:post, :put, :delete, :patch].freeze
|
8
|
+
CACHEABLE_METHODS = [:get, :head].freeze
|
9
|
+
|
10
|
+
# When was this request sent to the server
|
11
|
+
#
|
12
|
+
# @api public
|
13
|
+
attr_accessor :sent_at
|
14
|
+
|
15
|
+
# Inits a new instance
|
16
|
+
# @api private
|
17
|
+
def initialize(obj)
|
18
|
+
super
|
19
|
+
@requested_at = nil
|
20
|
+
@received_at = nil
|
21
|
+
end
|
22
|
+
|
23
|
+
# @return [HTTP::Request::Caching]
|
24
|
+
def caching
|
25
|
+
self
|
26
|
+
end
|
27
|
+
|
28
|
+
# @return [Boolean] true iff request demands the resources cache entry be invalidated
|
29
|
+
#
|
30
|
+
# @api public
|
31
|
+
def invalidates_cache?
|
32
|
+
INVALIDATING_METHODS.include?(verb) ||
|
33
|
+
cache_headers.no_store?
|
34
|
+
end
|
35
|
+
|
36
|
+
# @return [Boolean] true if request is cacheable
|
37
|
+
#
|
38
|
+
# @api public
|
39
|
+
def cacheable?
|
40
|
+
CACHEABLE_METHODS.include?(verb) &&
|
41
|
+
!cache_headers.no_store?
|
42
|
+
end
|
43
|
+
|
44
|
+
# @return [Boolean] true iff the cache control info of this
|
45
|
+
# request demands that the response be revalidated by the origin
|
46
|
+
# server.
|
47
|
+
#
|
48
|
+
# @api public
|
49
|
+
def skips_cache?
|
50
|
+
0 == cache_headers.max_age ||
|
51
|
+
cache_headers.must_revalidate? ||
|
52
|
+
cache_headers.no_cache?
|
53
|
+
end
|
54
|
+
|
55
|
+
# @return [HTTP::Request::Caching] new request based on this
|
56
|
+
# one but conditional on the resource having changed since
|
57
|
+
# `cached_response`
|
58
|
+
#
|
59
|
+
# @api public
|
60
|
+
def conditional_on_changes_to(cached_response)
|
61
|
+
self.class.new HTTP::Request.new(
|
62
|
+
verb, uri, headers.merge(conditional_headers_for(cached_response)),
|
63
|
+
proxy, body, version).caching
|
64
|
+
end
|
65
|
+
|
66
|
+
# @return [HTTP::Cache::Headers] cache control helper for this request
|
67
|
+
# @api public
|
68
|
+
def cache_headers
|
69
|
+
@cache_headers ||= HTTP::Cache::Headers.new headers
|
70
|
+
end
|
71
|
+
|
72
|
+
def env
|
73
|
+
{"rack-cache.cache_key" => ->(r) { r.uri.to_s }}
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
# @return [Headers] conditional request headers
|
79
|
+
# @api private
|
80
|
+
def conditional_headers_for(cached_response)
|
81
|
+
headers = HTTP::Headers.new
|
82
|
+
|
83
|
+
cached_response.headers.get("Etag")
|
84
|
+
.each { |etag| headers.add("If-None-Match", etag) }
|
85
|
+
|
86
|
+
cached_response.headers.get("Last-Modified")
|
87
|
+
.each { |last_mod| headers.add("If-Modified-Since", last_mod) }
|
88
|
+
|
89
|
+
headers.add("Cache-Control", "max-age=0") if cache_headers.forces_revalidation?
|
90
|
+
|
91
|
+
headers
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|